Защитить сервер по шагам

SSH: только ключи

Первое и самое важное на любом сервере с Linux — перестать принимать пароли по SSH. Боты пробуют их весь день; ключ подобрать нельзя. Откройте отдельный файл настройки, а не правьте основной, чтобы обновление пакета не перезаписало ваши настройки:

sudo vi /etc/ssh/sshd_config.d/00-hardening.conf

PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AllowUsers example

Файл начинается с 00- намеренно. sshd читает sshd_config.d/*.conf в порядке имён и берёт первое прочитанное значение для каждого ключевого слова, а многие облачные образы поставляют 50-cloud-init.conf с PasswordAuthentication yes, и его значение взяло бы верх над более поздним файлом.

PermitRootLogin no закрывает аккаунт root по SSH; PasswordAuthentication no принимает только ключи; AllowUsers перечисляет аккаунты, которым вообще можно входить. Проверьте файл, прежде чем применять его, потому что ошибка в синтаксисе может лишить вас доступа к серверу:

sudo sshd -t

Пустой вывод означает, что файл верен. Проверьте значения, которые sshd действительно использует, после чтения всех подключённых файлов:

sudo sshd -T | grep -E "^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) "

permitrootlogin no passwordauthentication no kbdinteractiveauthentication no

Оставьте текущую сессию открытой и применяйте изменение из второго терминала, чтобы в случае ошибки у вас остался путь назад:

sudo systemctl restart ssh

Затем попробуйте открыть новую сессию. Если она работает, закройте старую. Если нет, в старой сессии всё ещё есть root или sudo, а консоль провайдера — последняя надежда. Правила самого SSH — в статье SSH в Linux.

fail2ban

Ключи не пускают ботов внутрь, но они всё равно забивают журнал попытками. fail2ban читает журнал и блокирует адрес после нескольких неудач:

sudo apt install fail2ban

sudo vi /etc/fail2ban/jail.local

[sshd] enabled = true maxretry = 5 findtime = 10m bantime = 1h

sudo systemctl enable --now fail2ban

sudo fail2ban-client status sshd

Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 38 | `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd `- Actions |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 198.51.100.77

Пять неудач за десять минут дают час тишины. Блокировка живёт в файрволе, поэтому исчезает при перезагрузке, если fail2ban не запустится снова; enable об этом заботится. Никогда не блокируйте свой собственный адрес во время проверки: добавьте его в ignoreip в том же файле.

Файрвол

Политика по умолчанию должна отклонять всё входящее, что не разрешено правилом. В Ubuntu ufw делает это тремя командами, в таком порядке:

sudo ufw default deny incoming

sudo ufw allow OpenSSH

sudo ufw allow "Nginx Full"

sudo ufw enable

Добавляйте правило до того, как открыть новую службу, а не после. Когда в уроке 9 появится почта, почтовые порты добавят так же. Файрвол не заменяет службу, которая слушает только там, где должна: оба слоя должны согласовываться. Файрвол в Linux объясняет правила и то, как их читать.

Автоматические обновления безопасности

Большинство успешных атак используют дыру, которую закрыли месяцами раньше. Ubuntu умеет устанавливать обновления безопасности сама, и это самая действенная настройка на небольшом сервере:

sudo apt install unattended-upgrades

sudo vi /etc/apt/apt.conf.d/20auto-upgrades

APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1";

sudo systemctl list-timers apt-daily-upgrade.timer

NEXT LEFT LAST PASSED UNIT ACTIVATES Fri 2026-11-06 18:00:00 UTC 3h left Fri 2026-11-06 06:00:00 UTC 9h ago apt-daily-upgrade.timer apt-daily-upgrade.service

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

sudo unattended-upgrade --dry-run --debug

Starting unattended upgrades script Allowed origins are: o=Ubuntu,a=noble,o=Ubuntu,a=noble-security,... Packages that will be upgraded:

Последняя строка перечисляет пакеты, которые он установил бы; пустой список означает, что сервер уже актуален. Не проверяйте для этого unattended-upgrades.service: этот unit — только помощник выключения, а не задача, которая ставит обновления. По умолчанию автоматически ставятся только обновления из раздела безопасности. Обновление ядра вступает в силу только после перезагрузки, а сервер сам не перезагрузится: проверьте /var/run/reboot-required и планируйте перезагрузку. Статья об обновлениях описывает более широкую привычку.

Код сайта и его библиотеки

unattended-upgrades закрывает пакеты Ubuntu, а не код внутри папки сайта. Этому коду нужна своя дисциплина. Держите его в git и разворачивайте из репозитория, чтобы каждое изменение можно было проверить и откатить. Если сайт использует Composer, у него есть свои библиотеки; выполните эти две команды от пользователя сайта в папке проекта, затем обновите и протестируйте:

sudo -u example -H composer --working-dir=/var/www/example.com audit

sudo -u example -H composer --working-dir=/var/www/example.com outdated

audit перечисляет установленные пакеты с известными уязвимостями; outdated — те, у которых есть более новые версии. Обновите нужные, прогоните тесты сайта и разверните. Следите за сообщениями о безопасности фреймворка, CMS или библиотек, которые используете, потому что заметка о выпуске быстрее сканера. Удаляйте код и библиотеки, которые сайту больше не нужны: неиспользуемый код всё ещё доступен.

Если ваш сайт на WordPress, дополнительные шаги обновления перечислены в отдельной статье: Отдельная статья: WordPress (необязательно).

Только нужные службы

Каждая слушающая служба — это дверь. Перечислите их и для каждой ответьте на три вопроса: нужна ли она мне, кто до неё дотянется и обновлена ли она?

sudo ss -tlnp

LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6)) LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=812,fd=7)) LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=701,fd=3)) LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=944,fd=21))

Это здоровый список: два веб-порта для мира, SSH для вас и база данных, до которой может добраться только сама машина. Всё остальное — кандидат на удаление:

sudo systemctl disable --now apache2

Команда выше — обычный пример: сервер, где установлены и Nginx, и Apache, причём Apache после свежей загрузки всё ещё держит порт 80. Не удаляйте пакет, в котором не уверены; сначала выключите службу, перезагрузитесь и посмотрите, что ничего не сломалось.

База данных на localhost

MariaDB должна принимать подключения с машины, на которой работает, и больше ниоткуда. Проверьте настройку, а затем адрес прослушивания:

sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf

bind-address = 127.0.0.1

sudo systemctl restart mariadb

sudo ss -tlnp | grep 3306

LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=944,fd=21))

Пользователь базы данных привязан к хосту так же, как к имени: 'example_user'@'localhost' и 'example_user'@'%' — разные аккаунты. Создавайте только локальный. Если инструменту нужен удалённый доступ, используйте туннель SSH, а не открытый порт:

ssh -L 3307:127.0.0.1:3306 example@203.0.113.10

Туннель делает удалённую базу данных видимой на вашей машине как 127.0.0.1:3307, зашифрованной и аутентифицированной через SSH. На сервере ничего нового не слушает.

Один пользователь и один пул на сайт

Пул PHP по умолчанию запускает все сайты под одним пользователем — пользователем веб-сервера www-data. Дайте каждому сайту своего системного пользователя и свой пул, чтобы один сломанный сайт не мог прочитать файлы другого. Сайт из урока 5 уже принадлежит пользователю example, поэтому пул называет его напрямую; второй сайт получает своего системного пользователя, созданного так:

sudo adduser --system --group --home /var/www/shop.example.net shop

Затем пул первого сайта:

sudo vi /etc/php/8.3/fpm/pool.d/example.com.conf

[example.com] user = example group = example listen = /run/php/php8.3-fpm-example.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 chdir = /var/www/example.com php_admin_value[open_basedir] = /var/www/example.com:/tmp php_admin_value[disable_functions] = exec,passthru,shell_exec,system

sudo systemctl reload php8.3-fpm

Затем направьте блок сервера на новый сокет вместо сокета по умолчанию:

sudo vi /etc/nginx/sites-available/example.com

location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }

open_basedir ограничивает папки, которые PHP может открыть, а отключённые функции убирают самые простые способы выполнить команду оболочки. Некоторым плагинам нужна одна из них; убирайте из списка отключённых только те, в которых сайт действительно нуждается, и записывайте почему.

Права на файлы

Файлы принадлежат пользователю сайта, а группа веб-сервера может их читать, чтобы Nginx отдавал статические файлы. Больше никому ничего не нужно:

sudo chown -R example:www-data /var/www/example.com

sudo find /var/www/example.com -type d -exec chmod 750 {} \;

sudo find /var/www/example.com -type f -exec chmod 640 {} \;

Папкам нужен бит выполнения, чтобы в них можно было войти; файлам — нет. Файлы настройки с паролями строже:

sudo chmod 600 /var/www/example.com/config.php

600 означает, что его читает только владелец. Поскольку PHP работает под этим владельцем, сайт продолжает работать, а другой пользователь машины не видит ничего. open_basedir пула — /var/www/example.com:/tmp, поэтому PHP всё ещё может открыть файл из html/index.php. То же правило относится к файлу .env и к закрытому ключу.

TLS и заголовки безопасности

Certbot уже написал работающую настройку TLS, а файл, который он подключает, options-ssl-nginx.conf, уже задаёт современные протоколы и настройки сеанса:

grep -E "ssl_protocols|ssl_session" /etc/letsencrypt/options-ssl-nginx.conf

ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3;

Не повторяйте эти директивы в настройке сайта: второй ssl_prefer_server_ciphers или ssl_session_timeout заставит nginx -t упасть с ошибкой о повторяющейся директиве. Чего подключённый файл не добавляет, так это заголовков, которыми браузер защищает посетителей. Поместите эти четыре строки в блок server с listen 443 ssl, который написал certbot:

sudo vi /etc/nginx/sites-available/example.com

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "SAMEORIGIN" always;

add_header внутри блока location заменяет заголовки уровня сервера для этого location, поэтому location, которому нужен один из них, должен повторить все четыре, которые хочет.

HSTS велит браузеру использовать HTTPS для этого имени в течение года и отказываться от обычного HTTP-подключения, даже если ссылка просит его. Включайте его только когда HTTPS работает для каждого вашего поддомена, потому что отменить его трудно. Остальные три заголовка не дают браузеру угадывать тип файла, выдавать полный адрес предыдущей страницы и помещать ваш сайт в рамку на другом сайте. Актуальные рекомендации — на ssl-config.mozilla.org.

sudo nginx -t

sudo systemctl reload nginx

curl -sI https://example.com | grep -i strict-transport

strict-transport-security: max-age=31536000; includeSubDomains

Скрыть версии, добавить ограничения частоты

Ответ, который сообщает nginx/1.24.0 (Ubuntu), подсказывает атакующему, какие эксплойты пробовать первыми. Скройте это в блоке http, а версию PHP — в его настройке:

sudo vi /etc/nginx/nginx.conf

server_tokens off;

sudo vi /etc/php/8.3/fpm/php.ini

expose_php = Off

sudo systemctl reload php8.3-fpm

Скрытие версии само по себе не защита, но оно убирает бесплатную информацию. Настоящая защита от небольшого потока — ограничение частоты, которое считает запросы по адресу. Ставьте его на запросы, которые выполняют PHP, а не на весь сайт: обычная страница часто загружает десятки файлов CSS, JavaScript и картинок, и ограничение на всё это отказало бы нормальным посетителям. Задайте зону в блоке http:

sudo vi /etc/nginx/nginx.conf

limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;

Затем используйте её внутри location PHP сайта и, если у сайта есть страница входа, добавьте для неё отдельное и более строгое ограничение. Отвечайте на отказы кодом 429 вместо 503 по умолчанию:

sudo vi /etc/nginx/sites-available/example.com

location ~ \.php$ { limit_req zone=perip burst=20 nodelay; limit_req_status 429; include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; } location = /login.php { limit_req zone=perip burst=5 nodelay; limit_req_status 429; include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }

Десять запросов в секунду с одного адреса при всплеске до двадцати — щедро для человека и тесно для бота. Следите за журналом ошибок на отказы; если в нём появляются настоящие посетители, увеличьте числа:

sudo grep "limiting requests" /var/log/nginx/error.log

2026/11/06 10:00:00 [error] 1234#1234: *56 limiting requests, excess: 20.500 by zone "perip", client: 203.0.113.55, server: example.com, request: "POST /login.php HTTP/1.1"

Кэш перед сайтом или служба защиты от DDoS у провайдера справляются с тем, чего не может ограничение.

Аккаунты регистратора, провайдера и панели

Сервер — только один из аккаунтов, которые управляют сайтом. Остальные три — регистратор, хостинг-провайдер и панель управления, и каждый из них может потерять весь сайт одним щелчком.

Статья о двухфакторной аутентификации объясняет варианты и то, почему приложение аутентификатора или аппаратный ключ лучше кода по SMS. Статья о менеджерах паролей объясняет, как держать разный пароль для каждого из четырёх аккаунтов, не запоминая ни одного.

Резервные копии 3-2-1 с проверкой восстановления

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

Поместите команды в скрипт, который запускает root. Перенаправление вроде sudo mysqldump ... > /var/backups/... выполняется вашим собственным пользователем, а не root, поэтому запись в папку, доступную только root, падает с Permission denied. Внутри скрипта каждая команда уже root:

sudo vi /usr/local/sbin/backup-site

#!/bin/sh set -e mkdir -p /var/backups/site mysqldump --single-transaction example_db | gzip > /var/backups/site/db-$(date +%F).sql.gz rsync -a --delete /var/www/example.com/ /var/backups/site/files/ find /var/backups/site -name 'db-*.sql.gz' -mtime +14 -delete rsync -az /var/backups/site/ backup@198.51.100.30:/srv/backup/example.com/

sudo chmod 700 /usr/local/sbin/backup-site

Последняя строка копирует папку на другую машину и требует ключ SSH для backup@198.51.100.30 в /root/.ssh, потому что скрипт работает от root. Запустите его один раз вручную, затем поставьте в расписание на 03:00 каждую ночь через root cron:

sudo /usr/local/sbin/backup-site

sudo crontab -e

0 3 * * * /usr/local/sbin/backup-site

Затем докажите, что копия настоящая, в отдельном месте, а не поверх рабочих данных:

sudo mariadb -e "CREATE DATABASE restore_test;"

gunzip -c /var/backups/site/db-2026-11-06.sql.gz | sudo mariadb restore_test

sudo mariadb -e "SELECT COUNT(*) FROM restore_test.pages;"

+----------+ | COUNT(*) | +----------+ | 24 | +----------+

sudo mariadb -e "DROP DATABASE restore_test;"

Число строк, похожее на настоящее, означает, что дамп читается и полон. После этого удалите тестовую базу, как делает последняя команда. Для зашифрованных копий с дедупликацией в объектное хранилище обычные инструменты — restic и borg; метод описан в статье о резервных копиях, которые действительно восстанавливаются.

Мониторинг и журналы

Защита не закончена, пока что-то не скажет вам, что она перестала работать. Четыре проверки покрывают большую часть: сайт жив, диск не заполняется, сертификат достаточно свежий, свежая резервная копия достаточно недавняя. Внешняя проверка доступности, бесплатная или платная, делает первую; короткий скрипт — остальное.

df -h /

sudo certbot certificates | grep -E "Certificate Name|Expiry"

Certificate Name: example.com Expiry Date: 2027-02-04 09:00:00+00:00 (VALID: 89 days)

Журнал аутентификации — первое место, где виден взлом. Неудачные входы, новый пользователь, созданный в четыре утра, успешный вход из страны, где вы никогда не были:

sudo tail -n 10 /var/log/auth.log

2026-11-06T10:00:00.123456+00:00 example sshd[1234]: Failed password for invalid user admin from 198.51.100.77 port 51234 ssh2 2026-11-06T10:00:01.234567+00:00 example sshd[1236]: Accepted publickey for example from 203.0.113.55 port 51240 ssh2: ED25519 SHA256:...

sudo journalctl -u ssh --since "today" --no-pager | grep -c "Failed password"

0

Ноль — здоровый ответ, когда пароли отключены. Затем проверьте, что ни один файл в системных каталогах не изменился без обновления пакета: debsums сравнивает каждый установленный файл с контрольной суммой, записанной при установке. Опция -a добавляет файлы настройки, которые -c отдельно пропускает:

sudo apt install debsums

sudo debsums -ca

Пустой вывод — чисто. Изменённый файл в /usr/bin или /etc — серьёзный сигнал, и следующий раздел — ответ на него. Помните, что атакующий, у которого уже есть root, может изменить и сохранённые контрольные суммы, поэтому это быстрая проверка; AIDE, которая держит свою базу, — инструмент сильнее.

После взлома

Вы не будете чувствовать себя спокойно, поэтому следуйте списку. Порядок важнее скорости, потому что неверный шаг уничтожает доказательства, нужные, чтобы понять, что произошло.

  1. Изолируйте. Снимите снимок или образ диска, затем уберите машину из сети или заблокируйте весь трафик, кроме вашего адреса. Пока не перезагружайте: в памяти есть доказательства.
  2. Сохраните. Скопируйте журналы, список процессов, сетевые подключения и список задач по расписанию куда-нибудь вне машины. Запишите время, когда это заметили, в UTC.
  3. Считайте, что каждый секрет потерян. Смените пароли и ключи сайта, базы данных, почты, панели, регистратора и провайдера. Отзовите токены API и ключи SSH.
  4. Пересоберите с чистого образа. Возврат той же системы из её собственной копии возвращает и дверь, которой пользовался атакующий. Установите заново, затем восстановите только файлы сайта и базу данных, после их проверки.
  5. Найдите путь внутрь. Сравните файлы сайта с репозиторием, прочитайте журнал доступа вокруг первого странного запроса, проверьте новые unit systemd, задачи cron, ключи SSH и пользователей базы данных.
  6. Сообщите тем, кто должен знать. Провайдеру, если машина атакует других; клиентам или пользователям, если их данные раскрыты; в органы, если этого требует закон вашей страны.
  7. Напишите хронологию. Что случилось, в каком порядке, что вы изменили. Она превращает одну плохую неделю в урок для всей команды.

Самый частый путь обратно внутрь — дыра, которую так и не нашли. Если вы не можете объяснить, как атакующий вошёл, не доверяйте пересобранному серверу с тем же приложением, пока не обновите его и не прочитаете его журналы с первой минуты.

Тест

1 Какие две настройки отключают вход по паролю для SSH?

  1. PasswordAuthentication no и KbdInteractiveAuthentication no
  2. PermitRootLogin no и AllowUsers example
  3. PubkeyAuthentication yes и UsePAM yes
  4. Port 22 и ListenAddress
Ответ

a) PasswordAuthentication no закрывает парольный метод, а KbdInteractiveAuthentication no закрывает путь запрос-ответ, который может спросить тот же пароль. PermitRootLogin и AllowUsers ограничивают, кому можно входить, но пароль всё ещё работает для разрешённого аккаунта.

2 Почему SSH разрешают в файрволе до его включения?

  1. Потому что ufw нужен перезапуск
  2. Потому что политика по умолчанию отклоняет входящий трафик, включая вашу собственную сессию
  3. Потому что Nginx нужен порт 22
  4. Потому что этого требует fail2ban
Ответ

b) Включение файрвола с запретом по умолчанию без правила для SSH закрывает подключение, которым вы пользуетесь. Сначала разрешите, потом включайте.

3 Что unattended-upgrades устанавливает по умолчанию?

  1. Все пакеты, включая новые мажорные версии
  2. Обновления безопасности из раздела безопасности
  3. Только обновления ядра
  4. Ничего, он только скачивает
Ответ

b) По умолчанию выбран раздел безопасности. Обновления ядра устанавливаются, но требуют перезагрузки, а сервер сам её не делает.

4 Пул на сайт задаёт open_basedir. Что он делает?

  1. Ограничивает папки, которые может открыть PHP
  2. Ограничивает число запросов
  3. Шифрует файлы сайта
  4. Блокирует внедрение SQL
Ответ

a) Это граница файловой системы для PHP, чтобы ошибка на одном сайте не прочитала другой сайт или системный файл. Это не ограничение трафика и не фильтр запросов.

5 Какой режим прав подходит файлу с паролем базы данных, который читает только PHP?

  1. 777
  2. 644
  3. 600
  4. 755
Ответ

c) 600 означает, что только владелец читает и пишет. PHP работает под этим владельцем, поэтому сайт работает, а больше никто секрет не видит.

6 Что HSTS говорит браузеру?

  1. Дольше кэшировать картинки
  2. Использовать HTTPS для этого имени в течение заданного времени, даже если ссылка говорит HTTP
  3. Блокировать cookie
  4. Проверять сертификат каждый час
Ответ

b) Браузер запоминает заголовок и отказывается от обычного HTTP к этому имени. Это мощно и трудно отменяется, поэтому включайте, только когда у каждого поддомена есть HTTPS.

7 Зачем восстанавливать резервную копию в отдельную базу, прежде чем ей доверять?

  1. Потому что дамп зашифрован
  2. Потому что копию доказывает только успешное восстановление, а рабочую базу нельзя перезаписывать проверкой
  3. Потому что MariaDB отказывается импортировать дважды
  4. Потому что в дампе нет таблиц
Ответ

b) Проверка доказывает, что файл полон и читается, не рискуя сайтом. Копия, которую никто не восстановил, — не доказательство.

8 После взлома почему пересобирают с чистого образа, а не восстанавливают резервную копию сервера?

  1. Потому что копии медленнее
  2. Потому что в копии есть то, что пустило атакующего внутрь
  3. Потому что сертификат недействителен
  4. Потому что этого требует провайдер
Ответ

b) Системная копия переносит и компрометацию, включая изменённые двоичные файлы, ключи и задачи по расписанию. Пересоберите систему и восстановите только проверенные данные.

Автор статьи: Arthur Isaev

Изображение баннера