FreeBSD 15 как веб-хостинг: от установки до первого сайта с SSL

Itnull

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

FreeBSD 15 как веб-хостинг: от установки до первого сайта с SSL​


FreeBSD давно и заслуженно используется для веб-хостинга. Её выбирают за ZFS со снапшотами, jail-ы, стабильность и предсказуемые обновления. Ветка 15 принесла заметные изменения, поэтому даже опытным админам стоит разобраться, что поменялось.

В этой статье соберём полноценный хостинг-сервер на FreeBSD 15.1:

  • nginx + PHP-FPM с отдельным пулом и пользователем для каждого сайта;
  • MySQL на оптимально настроенном ZFS-датасете;
  • бесплатные SSL-сертификаты Let's Encrypt с автопродлением;
  • скрипт добавления нового сайта одной командой;
  • файрвол, защита от брутфорса, бэкапы и обновления.

0. Что нового во FreeBSD 15 для админа​


ИзменениеЧто это значит на практике
pkgbaseБазовую систему можно обновлять через pkg, как обычные пакеты. В 15.x это опция (технологическое превью), в 16 должна стать единственным способом
Срок поддержкиВетка поддерживается 4 года вместо 5. Минорные релизы нужно обновлять вовремя: 15.0 уже снята с поддержки, актуальна 15.1
blacklistd → blocklistdДемон защиты от брутфорса переименован, поменялись имена в конфигах
Репозиторий пакетовНазывается FreeBSD-ports (раньше просто FreeBSD), рядом появился FreeBSD-base

⚠️ Главное правило 15-й ветки: система управляется либо через freebsd-update, либо через pkgbase. Никогда не смешивайте два способа на одном сервере, иначе получите сломанную базовую систему.

1. Установка​


При установке с ISO выберите:

  • Файловая система: Auto (ZFS). Для хостинга это обязательно: снапшоты, сжатие, загрузочные окружения.
  • Способ установки базы:
    • Distribution Sets — классика с freebsd-update, максимально проверенный вариант для продакшена;
    • Packages (Tech Preview) — pkgbase. Будущее FreeBSD, обновления через pkg, но пока со статусом превью.
  • Сервисы: sshd, ntpd, dumpdev.
  • Hardening: отметьте все пункты, кроме тех, что вам точно мешают.

Дальше в статье команды обновления приведены для обоих вариантов.

2. Первичная настройка​


Обновление системы​


Классический вариант:

Код:
freebsd-update fetch install

pkgbase:

Код:
pkg update && pkg upgrade -r FreeBSD-base

Свежие пакеты вместо квартальных​


По умолчанию pkg использует ветку quarterly, которая обновляется раз в квартал (плюс исправления безопасности). Для веб-сервера часто нужны свежие версии PHP и nginx, поэтому переключимся на latest:

Код:
mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/FreeBSD.conf << 'EOF'
FreeBSD-ports: {
  url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest"
}
EOF

pkg update -f

Если вам важнее стабильность, чем свежесть, этот шаг можно пропустить: quarterly тоже получает исправления безопасности.

Администратор и SSH по ключу​


Код:
pw useradd admin -m -G wheel -s /bin/sh
passwd admin
mkdir -p /home/admin/.ssh
# вставьте свой публичный ключ:
ee /home/admin/.ssh/authorized_keys
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh && chmod 600 /home/admin/.ssh/authorized_keys

pkg install -y sudo
echo '%wheel ALL=(ALL:ALL) ALL' > /usr/local/etc/sudoers.d/wheel

Проверьте вход по ключу в новом окне, и только потом закрывайте пароли в /etc/ssh/sshd_config:

Код:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
UseBlocklist yes

Код:
service sshd restart

Почта root на свой ящик​


FreeBSD каждый день присылает отчёты о безопасности, уязвимых пакетах и состоянии дисков. Чтобы их получать, в /etc/mail/aliases:

Код:
root: you@example.com

Код:
newaliases

3. ZFS-датасеты под хостинг​


Разнесём сайты и базу по отдельным датасетам: так удобнее снапшотить, ограничивать место и настраивать параметры под нагрузку.

Код:
# Сайты: сжатие zstd отлично работает на PHP/HTML/CSS/JS
zfs create -o mountpoint=/usr/local/www/sites -o compression=zstd zroot/sites

# MySQL: размер записи под страницы InnoDB
zfs create -o mountpoint=/var/db/mysql \
    -o recordsize=16k -o compression=lz4 \
    -o logbias=throughput -o atime=off zroot/mysql

# Бэкапы дампов
zfs create -o mountpoint=/backup -o compression=zstd zroot/backup

Если хостите чужие сайты, можно ограничить место для отдельного клиента:

Код:
zfs create -o quota=10G zroot/sites/client1.com

4. Установка стека​


Код:
pkg install -y nginx \
    php84 php84-extensions php84-mysqli php84-pdo_mysql php84-gd \
    php84-curl php84-mbstring php84-intl php84-zip php84-exif \
    php84-sodium php84-opcache php84-pecl-redis \
    mysql84-server valkey

Имена пакетов зависят от актуальных версий. Если какой-то не найден, проверьте через pkg search php8 / pkg search mysql. Вместо MySQL можно поставить MariaDB (pkg search mariadb). Valkey — открытый форк Redis, полностью совместимый по протоколу, подходит для кэша XenForo, WordPress и других движков.

Включаем автозапуск:

Код:
sysrc nginx_enable="YES"
sysrc php_fpm_enable="YES"
sysrc mysql_enable="YES"
sysrc valkey_enable="YES"

5. Тюнинг ядра под веб​


FreeBSD умеет кое-что, чего нет в Linux. Например, accept filters: ядро будит nginx только тогда, когда HTTP-запрос пришёл целиком. Это экономит ресурсы и помогает против медленных атак.

/boot/loader.conf:

Код:
accf_http_load="YES"
accf_data_load="YES"
# современный TCP-стек RACK — лучше ведёт себя на плохих каналах
tcp_rack_load="YES"

/etc/sysctl.conf:

Код:
# Использовать RACK по умолчанию
net.inet.tcp.functions_default=rack

# Очередь входящих соединений
kern.ipc.soacceptqueue=4096

# Буферы сокетов
kern.ipc.maxsockbuf=16777216
net.inet.tcp.sendbuf_max=16777216
net.inet.tcp.recvbuf_max=16777216

# Быстрое освобождение соединений
net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.finwait2_timeout=5000

# Скрываем закрытые порты от сканеров
net.inet.tcp.blackhole=2
net.inet.udp.blackhole=1

Применяется после перезагрузки (модули) или командой service sysctl restart (sysctl).

6. MySQL​


Код:
service mysql-server start
mysql_secure_installation

В /usr/local/etc/mysql/my.cnf, в секции [mysqld]:

Код:
bind-address = 127.0.0.1

# ZFS уже гарантирует целостность записи — doublewrite не нужен
innodb_doublewrite = 0
innodb_flush_neighbors = 0

# ~50-70% RAM на выделенном сервере, меньше — если на нём ещё и PHP
innodb_buffer_pool_size = 2G
innodb_log_file_size = 512M

character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

Код:
service mysql-server restart

7. nginx: базовый конфиг​


/usr/local/etc/nginx/nginx.conf:

Код:
user  www;
worker_processes  auto;

events {
    worker_connections  4096;
    use kqueue;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout  30;
    server_tokens   off;
    client_max_body_size 64m;

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;

    # общие настройки TLS
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 1d;

    # заглушка: на IP и неизвестные домены — сброс соединения
    server {
        listen 80 default_server accept_filter=httpready;
        server_name _;
        return 444;
    }

    include /usr/local/etc/nginx/sites/*.conf;
}

Код:
mkdir -p /usr/local/etc/nginx/sites

Обратите внимание на accept_filter=httpready и use kqueue: это FreeBSD-специфичные оптимизации.

8. Изоляция сайтов: пользователь + пул PHP-FPM​


Главный принцип хостинга: каждый сайт работает от своего пользователя в своём пуле PHP-FPM. Если на одном сайте взломают плагин, злоумышленник не сможет прочитать файлы и конфиги соседей.

Структура:

Код:
/usr/local/www/sites/example.com/
├── public/    ← корень сайта (DocumentRoot)
├── logs/      ← логи nginx и PHP
└── tmp/       ← сессии и временные файлы PHP

Права:
  • владелец — пользователь сайта, PHP работает от его имени;
  • группа — группа сайта, в неё добавлен www, чтобы nginx мог читать статику;
  • остальные — нет доступа (750).

Стандартный пул www нам не нужен, отключаем его:

Код:
mv /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini

9. Скрипт добавления сайта​


Вся рутина — в одном скрипте. Создайте /usr/local/sbin/newsite:

Код:
#!/bin/sh
# Использование: newsite example.com
set -e

DOMAIN="$1"
[ -z "$DOMAIN" ] && { echo "Usage: $0 domain.com"; exit 1; }

USER=$(echo "$DOMAIN" | tr -c 'a-zA-Z0-9\n' '_' | cut -c1-16)
ROOT="/usr/local/www/sites/$DOMAIN"
POOL="/usr/local/etc/php-fpm.d/$DOMAIN.conf"
VHOST="/usr/local/etc/nginx/sites/$DOMAIN.conf"
SOCK="/var/run/php-fpm-$DOMAIN.sock"

# 1. Пользователь без shell
pw useradd "$USER" -d "$ROOT" -s /usr/sbin/nologin -c "site $DOMAIN"
pw groupmod "$USER" -m www

# 2. Каталоги (отдельный ZFS-датасет)
zfs create "zroot/sites/$DOMAIN"
mkdir -p "$ROOT/public" "$ROOT/logs" "$ROOT/tmp"
echo "<?php echo 'It works: $DOMAIN';" > "$ROOT/public/index.php"
chown -R "$USER:$USER" "$ROOT"
chmod 750 "$ROOT" "$ROOT/public"
chmod 770 "$ROOT/logs"
chmod 700 "$ROOT/tmp"

# 3. Пул PHP-FPM
cat > "$POOL" << EOF
[$DOMAIN]
user = $USER
group = $USER
listen = $SOCK
listen.owner = www
listen.group = www
listen.mode = 0660

pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
pm.max_requests = 500

php_admin_value[open_basedir] = $ROOT/public:$ROOT/tmp
php_admin_value[upload_tmp_dir] = $ROOT/tmp
php_admin_value[session.save_path] = $ROOT/tmp
php_admin_value[sys_temp_dir] = $ROOT/tmp
php_admin_value[error_log] = $ROOT/logs/php-error.log
php_admin_flag[log_errors] = on
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
EOF

# 4. Виртуальный хост nginx (пока только HTTP — для получения сертификата)
cat > "$VHOST" << EOF
server {
    listen 80 accept_filter=httpready;
    server_name $DOMAIN www.$DOMAIN;
    root $ROOT/public;
    index index.php index.html;

    access_log $ROOT/logs/access.log;
    error_log  $ROOT/logs/error.log;

    location /.well-known/acme-challenge/ { allow all; }

    location / {
        try_files \$uri \$uri/ /index.php?\$args;
    }

    location ~ \.php\$ {
        try_files \$uri =404;
        fastcgi_pass unix:$SOCK;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\. { deny all; }
}
EOF

# 5. Применяем
nginx -t
service php_fpm reload
service nginx reload

echo "Сайт $DOMAIN создан. Пользователь: $USER"
echo "Следующий шаг: certbot certonly --webroot -w $ROOT/public -d $DOMAIN -d www.$DOMAIN"

Код:
chmod 700 /usr/local/sbin/newsite
service php_fpm start
service nginx start

newsite example.com

База данных для сайта​


Код:
mysql -u root -p

CREATE DATABASE example_com;
CREATE USER 'example_com'@'localhost' IDENTIFIED BY 'сложный_пароль';
GRANT ALL PRIVILEGES ON example_com.* TO 'example_com'@'localhost';
FLUSH PRIVILEGES;

10. SSL от Let's Encrypt​


Код:
pkg search certbot          # узнать точное имя пакета
pkg install -y py311-certbot # подставьте имя из поиска

Получаем сертификат (DNS домена уже должен указывать на сервер):

Код:
certbot certonly --webroot \
    -w /usr/local/www/sites/example.com/public \
    -d example.com -d www.example.com \
    --email you@example.com --agree-tos

Сертификаты лежат в /usr/local/etc/letsencrypt/live/example.com/.

Дописываем в /usr/local/etc/nginx/sites/example.com.conf HTTPS-блок, а в HTTP-блоке включаем редирект:

Код:
server {
    listen 80 accept_filter=httpready;
    server_name example.com www.example.com;
    location /.well-known/acme-challenge/ {
        root /usr/local/www/sites/example.com/public;
    }
    location / { return 301 https://example.com$request_uri; }
}

server {
    listen 443 ssl accept_filter=dataready;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate     /usr/local/etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;

    root /usr/local/www/sites/example.com/public;
    index index.php index.html;

    access_log /usr/local/www/sites/example.com/logs/access.log;
    error_log  /usr/local/www/sites/example.com/logs/error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm-example.com.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2?)$ {
        expires 30d;
        access_log off;
    }

    location ~ /\. { deny all; }
}

Код:
nginx -t && service nginx reload

Автопродление: certbot ставит скрипт в periodic, его нужно только включить в /etc/periodic.conf:

Код:
weekly_certbot_enable="YES"
weekly_certbot_post_hook="service nginx reload"

Проверить продление вхолостую:

Код:
certbot renew --dry-run

11. PHP: OPcache и безопасность​


В /usr/local/etc/php.ini:

Код:
expose_php = Off
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60

opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 50000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2

session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

Код:
service php_fpm restart

12. Файрвол и blocklistd​


Минимальный /etc/pf.conf для хостинга:

Код:
ext_if = "vtnet0"
table <blocked> persist

set skip on lo0
set block-policy drop
scrub in on $ext_if all fragment reassemble

block log all
antispoof quick for $ext_if
anchor "blocklistd/*" in on $ext_if
block drop in quick on $ext_if from <blocked>

pass out quick on $ext_if keep state

pass in on $ext_if proto tcp to ($ext_if) port 22 flags S/SA keep state \
    (max-src-conn 10, max-src-conn-rate 5/30, overload <blocked> flush global)
pass in on $ext_if proto tcp to ($ext_if) port { 80, 443 } flags S/SA keep state
pass in on $ext_if proto udp to ($ext_if) port 443 keep state

pass in on $ext_if inet proto icmp icmp-type { echoreq, unreach } keep state
pass in on $ext_if inet6 proto ipv6-icmp keep state

Код:
sysrc pf_enable="YES" pflog_enable="YES"
sysrc blocklistd_enable="YES" blocklistd_flags="-r"

pfctl -nf /etc/pf.conf
nohup sh -c 'sleep 300 && pfctl -d' >/dev/null 2>&1 &
service pf start && service pflog start && service blocklistd start
# новый SSH работает? Отменяем таймер:
pkill -f "sleep 300"

Конфиг правил бана находится в /etc/blocklistd.conf, просмотр банов — blocklistctl dump -b.

Подробный разбор pf (GeoIP, Cloudflare, jail-ы, шейпинг) — в нашей серии статей «FreeBSD + pf». Правила оттуда работают и на 15-й ветке, нужно лишь заменить blacklistd на blocklistd.

13. Бэкапы​


Снапшоты ZFS​


Снапшот создаётся мгновенно и почти не занимает места. Это лучшая защита от «удалил не то» и от вирусов-шифровальщиков на сайтах.

Код:
pkg install -y sanoid

/usr/local/etc/sanoid/sanoid.conf:

Код:
[zroot/sites]
    use_template = production
    recursive = yes

[zroot/mysql]
    use_template = production

[template_production]
    hourly = 24
    daily = 14
    monthly = 3
    autosnap = yes
    autoprune = yes

В /etc/crontab:

Код:
*/15  *  *  *  *  root  /usr/local/bin/sanoid --cron

Восстановить один файл можно прямо из скрытого каталога снапшота:

Код:
ls /usr/local/www/sites/example.com/.zfs/snapshot/
cp /usr/local/www/sites/example.com/.zfs/snapshot/autosnap_..._hourly/public/wp-config.php \
   /usr/local/www/sites/example.com/public/

Дампы баз​


Снапшот работающей MySQL восстановится, но для надёжности делайте ещё и логические дампы. Скрипт /usr/local/sbin/dbbackup:

Код:
#!/bin/sh
DIR="/backup/mysql/$(date +%Y-%m-%d)"
mkdir -p "$DIR"
for db in $(mysql -N -e "SHOW DATABASES" | grep -Ev '^(information_schema|performance_schema|sys|mysql)$'); do
    mysqldump --single-transaction --routines --triggers "$db" | zstd -q > "$DIR/$db.sql.zst"
done
find /backup/mysql -type d -mtime +14 -exec rm -rf {} +

Пароль root для скрипта положите в /root/.my.cnf с правами 600:

Код:
[client]
user=root
password=пароль_root_mysql

Запуск каждую ночь:

Код:
30  3  *  *  *  root  /usr/local/sbin/dbbackup

Помните: бэкап на том же сервере — это не бэкап. Раз в сутки отправляйте /backup и снапшоты на другой сервер (zfs send | ssh ... zfs recv или syncoid из того же пакета sanoid).

14. Обновления без страха: загрузочные окружения​


ZFS позволяет перед обновлением сохранить всю систему целиком и откатиться одной командой, если что-то пошло не так.

Код:
# Перед любым обновлением системы
bectl create before-update-$(date +%Y%m%d)
bectl list

Обновление (классика):

Код:
freebsd-update fetch install
pkg upgrade

Обновление (pkgbase):

Код:
pkg upgrade -r FreeBSD-base
pkg upgrade -r FreeBSD-ports

Если после перезагрузки сервер не работает как надо:

Код:
bectl activate before-update-20261028
shutdown -r now

Или выберите нужное окружение прямо в загрузчике FreeBSD через KVM-консоль.

Сайты и база лежат в отдельных датасетах (zroot/sites, zroot/mysql), которые не входят в загрузочное окружение. Поэтому откат системы не откатит данные сайтов: так и задумано.

15. Ежедневное обслуживание​


Код:
pkg audit -F                  # уязвимости в установленных пакетах
pkg version -vl '<'           # что можно обновить
zpool status -x               # здоровье дисков
zfs list -o name,used,avail   # место по сайтам
sockstat -4l                  # что слушает порты
top -o res                    # кто ест память

Ротация логов сайтов. Добавьте в /etc/newsyslog.conf:

Код:
/usr/local/www/sites/*/logs/*.log  640  14  *  @T00  GJ  /var/run/nginx.pid  30

Это означает: ежедневная ротация, хранить 14 архивов со сжатием, затем nginx получает сигнал переоткрыть логи. Флаг G включает поддержку шаблона * в пути.

Что дальше​


У вас готов хостинг-сервер, на котором:

  • каждый сайт изолирован своим пользователем, пулом PHP и ZFS-датасетом;
  • новый сайт добавляется одной командой;
  • SSL выдаётся бесплатно и продлевается сам;
  • данные защищены снапшотами каждые 15 минут;
  • неудачное обновление откатывается за минуту.

Для следующего уровня изоляции, когда каждый клиент живёт в своём jail-е, присмотритесь к менеджеру Bastille (pkg install bastille) или к ручной схеме из нашей статьи «pf + jail-ы».

Вопросы и свой опыт пишите в теме!
 

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

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

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

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

Войти

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

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