Данила (Dayfing)
Назад к публикациям
3 011 слов16 мин

Защита Linux VPS: чек-лист SSH, файрвола, обновлений и мониторинга

В первый час на новом VPS с Ubuntu или Debian нужно закрыть то, что автоматические сканеры проверяют через несколько минут после появления IP-адреса в сети. Обновите систему, входите под обычным пользователем с sudo и ключом Ed25519, отключите вход по паролю и под root через drop-in-файл SSH, включите файрвол с запретом по умолчанию, который открывает только SSH и веб-порты, в идеале только для вашего CDN. Затем включите автоматические обновления безопасности с плановыми перезагрузками, добавьте fail2ban или CrowdSec, поместите приложение в песочницу systemd и настройте резервные копии и алерты до того, как DNS начнет указывать на сервер.

Команды ниже ориентированы на Ubuntu 24.04 LTS, а отличия Debian 12 и 13 отмечены отдельно. Пока вы работаете, держите открытой веб-консоль провайдера. Это ваш путь обратно на сервер, если изменение SSH или файрвола отрежет доступ.

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

Многие провайдеры выдают вход под root с паролем, поэтому первым делом замените и то и другое. Установите ожидающие обновления, затем создайте личную учетную запись с правами sudo:

apt update && apt full-upgrade -y
adduser deploy
usermod -aG sudo deploy

В облачных образах Ubuntu уже есть учетная запись ubuntu с правами sudo. В минимальной установке Debian с паролем root пакета sudo может не быть, поэтому сначала выполните apt install sudo. Заведите каждому человеку именную учетную запись, чтобы журнал аудита указывал на конкретного человека. UTC упрощает сопоставление логов между серверами и панелями CDN: timedatectl set-timezone Etc/UTC.

Чем Debian отличается от Ubuntu 24.04

Область Ubuntu 24.04 LTS Debian 12 и 13
Слушатель SSH ssh.socket запускает демон по запросу обычно ssh.service; проверьте через systemctl status
Файрвол ufw установлен, но неактивен ничего не включено; стандартный фреймворк nftables
Автообновления unattended-upgrades установлен и включен может отсутствовать; установите и включите
Журнал аутентификации /var/log/auth.log и журнал systemd только журнал; rsyslog не ставится по умолчанию с Debian 12
sudo установлен может отсутствовать в минимальной установке
OpenSSH 9.6 9.2 в Debian 12, 10.0 в Debian 13

SSH с ключами Ed25519 и drop-in-файлом hardening

Создайте и установите ключ

Генерируйте ключ на своей рабочей машине, никогда не на сервере. Ключи Ed25519 короткие, быстрые и поддерживаются любой актуальной версией OpenSSH. Защитите закрытый ключ парольной фразой и держите его в ssh-agent. Если у вас есть аппаратный ключ FIDO2, ssh-keygen -t ed25519-sk привяжет ключ к устройству.

ssh-keygen -t ed25519 -C "deploy@laptop-2026"
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]

ssh-copy-id требует одного входа по паролю. Если root уже принимает ваш ключ, а пароли отключены, скопируйте файл ключа на самом сервере:

sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys

Права доступа важны: при включенном по умолчанию StrictModes демон sshd игнорирует файлы ключей, в которые могут писать другие пользователи.

Напишите drop-in-файл

Файл /etc/ssh/sshd_config в Ubuntu начинается со строки Include /etc/ssh/sshd_config.d/*.conf, а руководство sshd_config говорит, что для каждого ключевого слова используется первое полученное значение. Drop-in-файлы читаются в лексическом порядке и побеждают остальную часть основного файла. Облачные образы часто содержат собственный файл вроде 50-cloud-init.conf или 60-cloudimg-settings.conf, который задает PasswordAuthentication, поэтому дайте своему файлу маленький префикс:

# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers deploy
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no

KbdInteractiveAuthentication no закрывает путь keyboard-interactive через PAM, который может запросить пароль даже при выключенном PasswordAuthentication. AllowUsers ограничивает вход именованными учетными записями. Основной файл Ubuntu задает X11Forwarding yes, и drop-in побеждает, потому что читается первым. Если проброс никому не нужен, добавьте AllowAgentForwarding no и AllowTcpForwarding no.

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

sudo sshd -t
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|kbdinteractive|authenticationmethods|allowusers'
sudo systemctl restart ssh

sshd -t ловит синтаксические ошибки, а sshd -T показывает конфигурацию, которая действительно применяется, в том числе не победил ли файл cloud-init. Перезапуск ssh сохраняет установленные сессии. В Ubuntu 24.04 слушающий сокет принадлежит ssh.socket, поэтому смена Port или ListenAddress требует еще sudo systemctl daemon-reload и sudo systemctl restart ssh.socket, причем после того, как файрвол разрешит новый порт. Остальные параметры описаны в руководстве Ubuntu по OpenSSH server.

Проверьте во второй сессии, прежде чем закрыть первую

Не закрывайте текущую сессию. Из нового терминала войдите как deploy по ключу и выполните sudo -v. Затем убедитесь, что закрытые пути больше не работают:

ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
ssh [email protected]

Обе попытки должны закончиться сообщением Permission denied (publickey). Только после этого закрывайте исходную сессию. Перенос SSH на другой порт уменьшает шум в логах, но мерой защиты не является.

Файрвол с запретом по умолчанию: ufw или nftables

Файрвол на хосте является вторым слоем после сетевого файрвола провайдера, если тот его предлагает. Отбрасывайте весь входящий трафик, кроме SSH и веб-портов, разрешайте исходящий и закрывайте IPv6 так же, как IPv4.

Ubuntu: ufw

Добавьте правило для SSH до включения файрвола, иначе ufw enable оборвет вашу сессию:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw allow proto tcp from any to any port 80,443 comment 'web'
sudo ufw enable
sudo ufw status verbose

Согласно руководству ufw, правило limit блокирует адрес, который открывает 6 или больше соединений за 30 секунд. Это тормозит сканеры, но может мешать автоматизации, открывающей много параллельных SSH-соединений; там лучше подходит обычный allow. Если у вас постоянный адрес администратора или VPN, разрешите SSH только с него: sudo ufw allow from 198.51.100.7 to any port 22 proto tcp. Правила действуют и для IPv6, пока в /etc/default/ufw стоит IPV6=yes, как по умолчанию.

Документация Docker предупреждает, что порты, опубликованные Docker, обходят правила ufw и firewalld. Публикуйте контейнеры на loopback, например -p 127.0.0.1:3000:3000, а наружу выставляйте reverse proxy.

Debian: nftables

В Debian стандартным фреймворком файрвола является nftables. Этот /etc/nftables.conf реализует ту же политику и заранее создает именованные наборы для диапазонов CDN из следующего раздела:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  set cloudflare_v4 {
    type ipv4_addr
    flags interval
  }

  set cloudflare_v6 {
    type ipv6_addr
    flags interval
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif "lo" accept
    meta l4proto { icmp, ipv6-icmp } accept
    tcp dport 22 accept
    ip saddr @cloudflare_v4 tcp dport { 80, 443 } accept
    ip6 saddr @cloudflare_v6 tcp dport { 80, 443 } accept
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }
}

Проверьте файл командой sudo nft -c -f /etc/nftables.conf, затем выполните sudo systemctl enable --now nftables. ICMPv6 оставляйте открытым: от него зависят обнаружение соседей и определение MTU пути в IPv6. flush ruleset удаляет и таблицы, созданные fail2ban или Docker, поэтому после перезагрузки правил перезапускайте эти службы. Используйте на хосте один инструмент управления файрволом.

Пускайте к веб-портам только CDN

Если сайт работает за Cloudflare или другим CDN, origin не должен отвечать никому другому на портах 80 и 443. Иначе любой, кто найдет адрес origin через старые DNS-записи, почтовый сервер на том же IP или сканирование интернета, обойдет WAF, лимиты запросов и кэш CDN. Cloudflare публикует свои диапазоны по адресам https://www.cloudflare.com/ips-v4 и https://www.cloudflare.com/ips-v6, а в документации по IP-адресам сказано, что новые диапазоны попадают в список до ввода в эксплуатацию.

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

#!/usr/bin/env bash
set -euo pipefail
for list in ips-v4 ips-v6; do
  curl -fsS "https://www.cloudflare.com/$list" |
    while read -r cidr || [ -n "$cidr" ]; do
      ufw allow proto tcp from "$cidr" to any port 80,443 comment 'cloudflare'
    done
done

Затем удалите открытое правило командой sudo ufw delete allow proto tcp from any to any port 80,443. В nftables обновляйте оба набора одной транзакцией, потому что nft -f применяет пакет целиком или не применяет ничего. Если загрузка не удалась, set -e остановит скрипт, а старые наборы останутся на месте:

#!/usr/bin/env bash
set -euo pipefail
v4=$(curl -fsS https://www.cloudflare.com/ips-v4 | paste -sd, -)
v6=$(curl -fsS https://www.cloudflare.com/ips-v6 | paste -sd, -)
nft -f - <<EOF
flush set inet filter cloudflare_v4
flush set inet filter cloudflare_v6
add element inet filter cloudflare_v4 { $v4 }
add element inet filter cloudflare_v6 { $v6 }
EOF

Запускайте его при загрузке после nftables.service и раз в неделю по таймеру systemd. Nginx после этого нужны set_real_ip_from для тех же диапазонов и real_ip_header CF-Connecting-IP;, иначе логи и лимиты будут видеть адреса Cloudflare вместо посетителей. Серверную сторону разбирает руководство по кэшированию в Nginx и Cloudflare.

Список разрешенных IP доказывает, что соединение пришло из сети Cloudflare, но не то, что оно относится к вашей зоне. Руководство Cloudflare по защите origin называет такой список уязвимым к подмене IP и описывает более сильные варианты: Authenticated Origin Pulls, которым нужен режим шифрования Full или Full (strict), и Cloudflare Tunnel, которому входящие веб-порты вообще не нужны.

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

Ubuntu Server по умолчанию устанавливает unattended-upgrades и включает обновления безопасности, как описано в руководстве по автоматическим обновлениям. Проверьте это:

cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade --dry-run -v

Файл должен содержать APT::Periodic::Update-Package-Lists "1"; и APT::Periodic::Unattended-Upgrade "1";. В Debian выполните sudo apt install unattended-upgrades и sudo dpkg-reconfigure -plow unattended-upgrades.

Исправления ядра начинают действовать только после перезагрузки. В Ubuntu 24.04 needrestart перезапускает службы, использующие обновленные библиотеки, но заменить работающее ядро он не может. Когда пакету нужна перезагрузка, появляется /var/run/reboot-required. Храните локальные настройки в файле, который сортируется после пакетного 50unattended-upgrades:

// /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:30";

Выберите время с низким трафиком и убедитесь, что приложение поднимается после загрузки без ручных действий. Логи лежат в /var/log/unattended-upgrades/. Canonical Livepatch, бесплатный для личного использования на пяти машинах, закрывает критические и высокие уязвимости ядра прямо в памяти, но Canonical прямо пишет, что перезагрузку он не заменяет.

fail2ban или CrowdSec

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

fail2ban есть в репозиториях обоих дистрибутивов и не требует учетной записи. В Ubuntu 24.04 пакет включает jail sshd с бэкендом журнала systemd и действиями nftables, и такой бэкенд подходит и для Debian, где нет auth.log. Установите его командой sudo apt install fail2ban, затем настройте jail в локальном файле, а не в jail.conf:

# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
bantime.increment = true

Примените файл через sudo systemctl restart fail2ban и посмотрите состояние jail командой sudo fail2ban-client status sshd.

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

curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec crowdsec-firewall-bouncer-nftables
sudo cscli collections list
sudo cscli decisions list

За CDN веб-запросы приходят с адресов CDN. Блокировка HTTP-атакующего в файрволе хоста либо отрежет узел Cloudflare, либо коснется адреса, который никогда не подключается напрямую. Оставьте блокировки на хосте для SSH, а для HTTP используйте правила WAF и ограничения запросов в CDN.

Песочница systemd для приложения

Запускайте бэкенд от отдельного системного пользователя в unit-файле systemd, а не в shell или через nohup, и пусть systemd отнимет у процесса все, что ему не нужно. Создайте пользователя командой sudo useradd --system --no-create-home --shell /usr/sbin/nologin webapp. Этот unit запускает сервис на Node.js на localhost за Nginx:

# /etc/systemd/system/webapp.service
[Unit]
Description=Example web application
After=network-online.target
Wants=network-online.target

[Service]
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/node /opt/webapp/server.js
Environment=HOST=127.0.0.1 PORT=3000
LoadCredential=db_password:/etc/webapp/db_password
Restart=on-failure
StateDirectory=webapp
UMask=0077

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

[Install]
WantedBy=multi-user.target

NoNewPrivileges=yes не дает процессу и его потомкам получить привилегии через setuid-файлы или файловые capabilities. ProtectSystem=strict монтирует файловую систему для сервиса только на чтение, кроме /dev, /proc и /sys, поэтому единственное место для записи — /var/lib/webapp из StateDirectory=. Для остального добавьте ReadWritePaths=. ProtectHome=yes скрывает /home, /root и /run/user, а PrivateTmp=yes дает сервису собственные /tmp и /var/tmp. Пустой CapabilityBoundingSet= убирает все capabilities, а @system-service является списком разрешенных вызовов, который руководство systemd.exec рекомендует как отправную точку.

MemoryDenyWriteExecute=yes пропущен намеренно: в руководстве сказано, что он несовместим с JIT-движками, а V8 в Node.js генерирует код во время выполнения. Для сервисов на Go, Rust или C добавьте его и протестируйте.

В Ubuntu 24.04 с systemd 255 команда systemd-analyze security --offline=true оценила этот файл в 1.5 против 9.0 для такого же unit с одним только User=. Запустите сервис и проверьте:

sudo systemctl daemon-reload
sudo systemctl enable --now webapp
systemd-analyze security webapp.service
journalctl -u webapp -b -n 50 --no-pager

Если сервис не стартует, журнал обычно называет заблокированный путь или системный вызов. Ослабляйте по одному параметру, а не удаляйте весь блок. Пакетные службы вроде Nginx запускаются от root и требуют определенных capabilities, поэтому укрепляйте их через sudo systemctl edit nginx, тоже по одному параметру.

Синхронизация времени, journald и хранение логов

Проверка TLS, коды TOTP, расписания резервного копирования и сопоставление логов требуют точных часов. Ubuntu 24.04 и Debian 12 по умолчанию используют systemd-timesyncd. Выполните timedatectl status и найдите строки System clock synchronized: yes и NTP service: active. Если NTP-служба не работает, установите systemd-timesyncd или chrony.

При стандартном Storage=auto журнал хранится на диске, только если существует /var/log/journal, и может занимать до 10 процентов файловой системы, но не больше 4 ГБ. Задайте явные лимиты, чтобы шумный сервис не заполнил маленький диск, а срок хранения соответствовал вашей политике конфиденциальности:

# /etc/systemd/journald.conf.d/50-retention.conf
[Journal]
Storage=persistent
SystemMaxUse=1G
MaxRetentionSec=30day

Примените его через sudo systemctl restart systemd-journald и проверьте journalctl --disk-usage. Логи Nginx и приложения в /var/log ротирует logrotate, поэтому приведите /etc/logrotate.d/ к тому же сроку. Примечания к выпуску Debian 12 подтверждают, что rsyslog больше не устанавливается по умолчанию, так что в Debian журнал systemd является единственным системным логом. Логи доступа содержат IP-адреса, которые по GDPR могут быть персональными данными, поэтому более короткий срок хранения часто и есть правильный.

Секреты и права доступа к файлам

Секреты на VPS обычно утекают обыденными путями: файл .env, доступный на чтение всем пользователям, ключ в истории Git или переменная окружения, попавшая в отчет о сбое. Руководство systemd прямо говорит, что переменные окружения не подходят для секретов: они доступны непривилегированным клиентам через D-Bus и наследуются дочерними процессами. Используйте LoadCredential=, как в unit выше. Исходный файл остается доступным только root, а сервис получает копию только для чтения в $CREDENTIALS_DIRECTORY:

sudo install -d -m 0700 -o root -g root /etc/webapp
sudo install -m 0600 -o root -g root /dev/null /etc/webapp/db_password
sudoedit /etc/webapp/db_password

systemd-creds encrypt вместе с LoadCredentialEncrypted= дополнительно хранит файл зашифрованным ключом хоста или TPM. Код приложения должен принадлежать root или пользователю для деплоя, но не пользователю сервиса, чтобы скомпрометированный процесс не мог переписать сам себя. После установки стороннего ПО ищите файлы, доступные на запись всем, и неожиданные setuid-файлы:

sudo find / -xdev -type f -perm -0002 -print
sudo find / -xdev -type f -perm -4000 -print

Ключу деплоя из CI выделите отдельную строку в authorized_keys с опцией restrict и, где возможно, списком адресов from=. Агентам для программирования, которые выполняют команды на сервере, нужна та же дисциплина: отдельный пользователь, никаких production-секретов и подтверждение разрушительных команд. Руководство по агентному программированию сравнивает механизмы разрешений в современных CLI.

Резервные копии и хуки мониторинга

Hardening снижает вероятность инцидента, а резервные копии и алерты определяют, сколько он продлится. Снимки провайдера удобны для быстрого отката, но живут в том же аккаунте, что и сервер, поэтому независимой копией не являются. Держите зашифрованную версионированную копию вне провайдера и проверяйте восстановление. Руководство по резервному копированию 3-2-1 с restic разбирает структуру репозитория, хранение и учения по восстановлению.

Одному VPS нужно несколько сигналов: внешняя HTTP-проверка через CDN, проверка того, что origin отказывает в прямых подключениях, заполненность диска, срок действия сертификатов, упавшие unit systemd и heartbeat от каждой плановой задачи, чтобы молча остановившийся бэкап вызвал алерт. systemd умеет вызывать уведомление при сбое любого unit:

# /etc/systemd/system/[email protected]
[Unit]
Description=Alert on failure of %i

[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure %i

Добавьте OnFailure=notify-failure@%n.service в секцию [Unit] unit-файлов приложения и резервного копирования. Скрипт может отправлять имя unit и последние строки журнала в webhook чата или системы оповещений. Экспортеры метрик, например Prometheus node exporter, должны слушать localhost или частную сеть. Если на сервере работают AI-агенты или MCP-инструменты с доступом к shell, выделите им отдельного пользователя и песочницу и считайте их входные данные недоверенными, как объясняет руководство по prompt injection и безопасности MCP.

Чек-лист на первый час

  1. Установите обновления, создайте именного пользователя с sudo и держите консоль провайдера открытой.
  2. Установите ключ Ed25519 и добавьте 00-hardening.conf с отключенным входом под root и по паролю.
  3. Выполните sshd -t и sshd -T, перезапустите ssh и проверьте новую сессию до закрытия старой.
  4. Включите файрвол с запретом по умолчанию для IPv4 и IPv6, открыв SSH, 80 и 443.
  5. Ограничьте веб-порты диапазонами Cloudflare или используйте Authenticated Origin Pulls либо Tunnel и восстановите IP посетителей в Nginx.
  6. Проверьте автоматические обновления безопасности и настройте автоматические перезагрузки.
  7. Включите fail2ban или CrowdSec для SSH, а для HTTP используйте WAF в CDN.
  8. Запустите приложение от отдельного пользователя в песочнице systemd и проверьте systemd-analyze security.
  9. Проверьте синхронизацию времени и задайте лимиты размера и срока хранения журнала.
  10. Перенесите секреты в файлы credentials, доступные только root, и проверьте права доступа.
  11. Настройте резервные копии вне провайдера, проверьте восстановление и добавьте алерты о сбоях и heartbeat.
  12. Один раз перезагрузите сервер намеренно и убедитесь, что все службы вернулись без ручных действий.

Ещё публикации