У першу годину на новому 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.
Чеклист на першу годину
- Установіть оновлення, створіть іменного користувача з sudo і тримайте консоль провайдера відкритою.
- Установіть ключ Ed25519 і додайте
00-hardening.confз вимкненим входом під root і за паролем. - Виконайте
sshd -tіsshd -T, перезапустітьsshі перевірте нову сесію до закриття старої. - Увімкніть файрвол із забороною за замовчуванням для IPv4 і IPv6, відкривши SSH, 80 і 443.
- Обмежте вебпорти діапазонами Cloudflare або використайте Authenticated Origin Pulls чи Tunnel і відновіть IP відвідувачів у Nginx.
- Перевірте автоматичні оновлення безпеки й налаштуйте автоматичні перезавантаження.
- Увімкніть fail2ban або CrowdSec для SSH, а для HTTP використовуйте WAF у CDN.
- Запустіть застосунок від окремого користувача в пісочниці systemd і перевірте
systemd-analyze security. - Перевірте синхронізацію часу й задайте ліміти розміру та строку зберігання журналу.
- Перенесіть секрети у файли credentials, доступні лише root, і перевірте права доступу.
- Налаштуйте резервні копії поза провайдером, перевірте відновлення й додайте алерти про збої та heartbeat.
- Один раз навмисно перезавантажте сервер і переконайтеся, що всі служби повернулися без ручних дій.