Шпаргалка по первичной настройке свежего облачного сервера на Ubuntu 26.04: проверка системы, обновления и автообновления, swap, отдельный пользователь, вход по SSH-ключу, харденинг SSH, UFW и fail2ban. Это не экспертный гайд, а то, что я сам сделал и в чём разобрался, пока поднимал сервер для своего сайта.
Исходные данные
Облачный сервер (KVM) под личный сайт-карточку, блог и отдельное фронт + бэк приложение: 2 CPU, 2 ГБ RAM, 40 ГБ диска, Ubuntu 26.04. Все команды прогнаны на отдельном чистом демо-сервере с пользователем demo и хостом DemoServer; <IP-сервера> подставляем свой.
Доступ к серверу
Есть несколько способов доступа к серверу, кратко о них.
Консоль провайдера
На сайте провайдера есть постоянный доступ к твоему серверу.
- Плюсы: если что-то неправильно настроено или не получается зайти на сервер со своего хоста (частая проблема: включили UFW, а порт 22 не разрешили), снаружи на сервер не попасть, а через консоль доступ есть всегда.
- Минусы: консоль в целом не очень удобна, у многих провайдеров бывают задержки ввода-вывода.
- Итог: рекомендую как запасной вариант, который всегда поможет восстановиться.
SSH из PowerShell
Через консоль операционной системы; у меня Windows, поэтому PowerShell. По умолчанию вход по логину и паролю root:
ssh root@<IP-сервера>
💡 <IP-сервера> здесь и дальше — адрес своего сервера из панели провайдера, заменяем целиком, вместе с угловыми скобками.
После этой команды будет предложено ввести пароль (при вводе символы не отображаются, но то, что ты вводишь, принимается):
root@<IP-сервера>'s password:
При правильном пароле мы заходим на сервер. Рекомендуется завести отдельного пользователя с правом sudo и не работать под root. Это будет сделано дальше (раздел 5).
Вход по SSH-ключу (приватный + публичный ключ) настраивается в разделе 6. У него есть свои плюсы и минусы, но мне больше нравится этот вариант.
PuTTY
Приложение для доступа к серверам: можно сохранять адреса и подключаться одним кликом. Отличная программа, но я её не прочувствовал, поэтому пользуюсь PowerShell.
1. Разведка системы
1.1. Проверка операционной системы
После удачного входа мы уже находимся в консоли сервера.
cat /etc/os-release
Проверяем дистрибутив и версию. У меня вот такой вывод:
PRETTY_NAME="Ubuntu 26.04.1 LTS" <- Полное название
NAME="Ubuntu" <- Наименование ОС
VERSION_ID="26.04" <- Major.Minor версия выпуска
VERSION="26.04.1 LTS (Resolute Raccoon)"
VERSION_CODENAME=resolute
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=resolute
LOGO=ubuntu-logo
1.2. Проверка способа виртуализации
Следующая команда проверит способ виртуализации твоего сервера.
systemd-detect-virt
systemd-detect-virt
- ✅
kvm/qemu— полноценная VM, доступно всё; предпочтительный выбор, сейчас стандарт у большинства провайдеров - ⚠️
lxc/openvz— общее ядро, swap и sysctl могут быть недоступны; можно работать, но не все функции будут доступны + делишь ресурсы с другими такими же пользователями. Все реже встречается у провайдеров. Если такой, то лучше найти первый вариант. У меня были проблемы с корректной работой UFW на Ubuntu 24.04, и сталкивался с ними только у одного провайдера. - ❌
docker— ты внутри контейнера, systemd не работает → менять провайдера/тариф. Худший из вариантов. Я ни разу не встречал у провайдера, но вполне возможно на собственных серверах.
Итог: если kvm, то отлично, менять ничего не нужно; если openvz, работать можно, но я бы рекомендовал сменить (использовать, только если бюджет очень ограничен); если docker, то ищем провайдера или меняем тариф: нам под изначальные условия не подходит.
2. База системы
💡 Разделы 2–4 выполняем под root, sudo не нужен.
2.1. Обновление системы
В этом пункте проверяем и устанавливаем обновления системы. Образ для ОС может быть создан и загружен очень давно провайдером, после этого могут быть важные обновления безопасности и компонентов, поэтому это делаем первым шагом.
apt update # обновить каталог пакетов
apt upgrade -y # установить обновления
apt update
Проверяет наличие обновлений: если будут обновления, то в конце будет запись:
9 packages can be upgraded. Run 'apt list --upgradable' to see them.
apt upgrade -y
Скачивает и устанавливает обновления. Флаг -y указывает на то, что мы согласны с установкой, и вопрос о разрешении установки у системы не возникает; без него будет задан вопрос о разрешении. В итоге получаем список обновленных пакетов в этом цикле.
При вызове команды возможно сообщение:
Not upgrading yet due to phasing:
openssh-client openssh-server openssh-sftp-server
Это не ошибка: это обновления, которые выкатываются не всем машинам сразу, а постепенно: сначала небольшой части машин, потом всё большей, и так до 100% (документация Ubuntu). В метаданных пакета есть поле, по которому идет отбор. Попадёт ли конкретный сервер в текущую волну, apt решает сам по хешу от /etc/machine-id и версии пакета.
Если по новой версии начинают сыпаться отчёты об ошибках (Ubuntu собирает их с машин), раздачу пакета останавливают. Получается проверка на уровне дистрибутива.
Важно:
Security-обновления не фазируются и приходят сразу. Под фазирование попадают обычные багфиксы.
Через несколько дней пакеты поставятся сами при очередном apt upgrade.
После установки проверяем, нужна ли перезагрузка сервера. Некоторые обновления требуют перезагрузки сервера.
ls -l /var/run/reboot-required
ls -l /var/run/reboot-required
- ✅
No such file or directory→ ребут не нужен - ⚠️ Файл есть → обновилось ядро или системная библиотека,
rebootв удобное время. Какие пакеты его требуют:cat /var/run/reboot-required.pkgs
2.2. Проверка настроек времени и даты сервера
Дальше проверим настройки даты и времени для сервера.
timedatectl
timedatectl
- ✅
System clock synchronized: yes+NTP service: active - ❌
synchronized: no→timedatectl set-ntp true. Если не помогло: в Ubuntu 26.04 время синхронизирует chrony (неsystemd-timesyncd), проверяемsystemctl status chronyиchronyc tracking(строкаLeap status: Normal— синхронизация есть) - 💡 На серверах держим UTC — нет перехода на летнее время, логи сходятся между хостами
У меня такой вывод получился:
Local time: Thu 2026-10-08 09:27:58 UTC
Universal time: Thu 2026-10-08 09:27:58 UTC
RTC time: Thu 2026-10-08 09:27:58
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Соответственно, все в порядке, можно ничего не менять.
2.3. Смена имени хоста
Сейчас у нас неинформативное имя сервера, что-то вроде ceca4d3598b6, оно присваивается при создании провайдером. Рекомендую переименовать для читаемости и мониторинга, если будем его подключать.
hostnamectl set-hostname DemoServer
DemoServer — название хоста, на которое хотим сменить.
После смены hostname — обязательно:
Новое имя нужно прописать в /etc/hosts, иначе система не сможет его разрешить. Сначала проверяем, есть ли там уже строка с адресом 127.0.1.1:
grep 127.0.1.1 /etc/hosts
- Вывод пустой → строки нет, добавляем её.
- Есть строка вроде
127.0.1.1 ceca4d3598b6→ в ней старое имя от провайдера, заменяем его.
Если строка есть, заменяем в ней имя:
sed -i 's/^127\.0\.1\.1.*/127.0.1.1 DemoServer/' /etc/hosts
sed -i правит файл на месте. Выражение s/что/на что/ находит строку, которая начинается с 127.0.1.1, и целиком заменяет её на 127.0.1.1 DemoServer. Точки экранированы \., потому что в регулярном выражении точка означает «любой символ».
Если строки нет, добавляем её в конец файла:
echo "127.0.1.1 DemoServer" >> /etc/hosts
⚠️ Обязательно два >: >> дописывает в конец файла, а один > перезапишет /etc/hosts целиком, и все остальные записи пропадут.
Проверяем, что имя разрешается:
hostname -f
- ✅ Отвечает мгновенно
- ❌ Думает 5–10 сек → нет записи в
/etc/hosts,sudoбудет тормозить при каждом вызове
💡 После reboot имя и строка в /etc/hosts сохранились: cloud-init их не трогает (в /etc/cloud/cloud.cfg нет preserve_hostname и manage_etc_hosts). Если на другом сервере имя откатится — добавить туда preserve_hostname: true.
3. Автообновления
В этом разделе настроим автообновления для системы. Это нужно для обновлений безопасности системы: найденные эксплойты или ошибки публикуются и разбираются довольно быстро, и если их не обновлять, то ими могут воспользоваться злоумышленники, а ты на свой сервер заходишь раз в месяц. Автообновления делают это за тебя. Для этого используем unattended-upgrades — штатный механизм Debian и Ubuntu, который сам скачивает и ставит обновления пакетов по расписанию, без твоего участия. Для начала установим необходимый пакет.
apt install unattended-upgrades -y
Следующая команда показывает, включён ли механизм. В файле два флага:
APT::Periodic::Update-Package-Lists "1"— ежедневно обновлять списки пакетов (аналогapt update);APT::Periodic::Unattended-Upgrade "1"— ежедневно ставить обновления.
cat /etc/apt/apt.conf.d/20auto-upgrades
Следующая команда показывает systemd-таймер, который запускает установку: когда он срабатывал последний раз и когда сработает следующий. Это проверка того, что расписание реально работает, а не только прописано в конфиге.
systemctl list-timers apt-daily-upgrade.timer --no-pager
cat 20auto-upgrades
- ✅ Обе строки со значением
"1"(Update-Package-Lists и Unattended-Upgrade) - ❌
"0"или файла нет →dpkg-reconfigure -plow unattended-upgrades
💡 Ставятся только security-обновления. Авторебут выключен — следить за /var/run/reboot-required.
4. Swap
Swap — область на диске, куда ядро выгружает редко используемые страницы оперативной памяти, когда RAM заканчивается. На сервере с 1–4 ГБ памяти это прежде всего страховка: без swap пиковая нагрузка (сборка npm/dotnet, docker build, всплеск трафика) сразу приведет к тому, что оперативной памяти не хватит, и процесс будет закрыт, не завершившись. Со swap система замедлится, но продолжит работать.
В качестве swap можно использовать отдельный раздел диска или swap-файл на созданном разделе. Раздел можно создать при установке системы или подключить отдельно, но его не расширить так просто, в отличие от файла. Поэтому на VPS файл — обычно единственный вариант.
4.1. Создание swap-файла
Проверяем, нет ли уже swap; если вывод пустой, то ничего нет:
swapon --show
Далее создадим swap-файл:
fallocate -l 4G /swapfile # создать (мгновенно)
chmod 600 /swapfile # ОБЯЗАТЕЛЬНО до активации
mkswap /swapfile # разметить
swapon /swapfile # активировать
echo '/swapfile none swap sw 0 0' | tee -a /etc/fstab
⚠️ chmod 600 обязателен — в swap попадает содержимое RAM: пароли, токены, env-переменные. С правами 644 читается любым пользователем.
4.2. Проверка fstab
Ошибка в /etc/fstab может отправить систему в emergency mode при загрузке. Поэтому, прежде чем перезагружаться, проверяем запись двумя способами: сначала статически, потом вживую.
1. Статическая проверка — разбирает fstab без монтирования: синтаксис, существование источников, типы ФС, опции.
systemctl daemon-reload
findmnt --verify --verbose
- ✅
0 parse errors, 0 errors, N warnings— всё в порядке - ⚠️
non-bind mount source /swapfile is a directory or regular file— ложное срабатывание:findmntожидает блочное устройство и не знает про swap-файлы. Это warning, а не error, его можно игнорировать - ⚠️
fstab has been modified, but systemd still uses the old version— systemd не перечиталfstabпосле изменений, выполниsystemctl daemon-reload - ❌ Любые
errors> 0 — исправить запись до перезагрузки
2. Живая проверка — отключаем swap и поднимаем его заново из fstab:
free -h # убедиться, что swap пуст или RAM хватит
swapoff /swapfile && swapon -a && swapon --show
- ✅
/swapfileснова в списке — запись вfstabрабочая - ❌ Пустой вывод — swap не поднялся из
fstab, проверь строку черезgrep swapfile /etc/fstab
⚠️ swapoff возвращает в RAM всё содержимое swap. Сразу после создания это безопасно, swap пуст. На живом сервере с заполненным swap и нехваткой памяти команда может зависнуть или вызвать OOM-killer.
💡 При загрузке swap поднимает не swapon -a, а systemd-unit, сгенерированный из fstab. Для полной уверенности проверь его:
systemctl list-units --type swap # swapfile.swap должен быть active
4.3. Настройка swappiness
Swap создан, но по умолчанию ядро пользуется им слишком охотно. За это отвечает параметр vm.swappiness. Он задаёт приоритет: что выгодней освободить, когда память заканчивается — сбросить кэш файлов или отправить на диск память работающих процессов. Чем выше значение, тем раньше приложения отправляются в swap. А чтение с диска даже на NVMe на порядки медленнее RAM.
cat /proc/sys/vm/swappiness # текущее значение
echo 'vm.swappiness=10' | tee /etc/sysctl.d/99-swappiness.conf # сохранить (переживёт ребут)
sysctl --system # применить сейчас
cat /proc/sys/vm/swappiness # проверить
- ⚠️
60— значение по умолчанию, рассчитанное на десктоп. На сервере это значит, что ядро начнёт выгружать память приложений раньше, чем нужно, и сервисы будут проседать по отклику - ✅
10— swap работает как страховка от OOM, а не как продолжение оперативной памяти - ❌
0— не отключает swap, а откладывает его до критической нехватки памяти. К этому моменту кэш уже вытеснен, система тормозит, и всё часто заканчивается OOM-killer’ом
💡 systemctl daemon-reload здесь не нужен: sysctl-параметры применяет sysctl --system, systemd в этом не участвует.
Настройки безопасности сервера
Далее в статье будут описаны настройки базовой безопасности сервера.
Безопасность сервера формируется не из одного пункта: выстраивается эшелонированная защита. В этой статье рассмотрим основные настройки безопасности, но их может быть больше. Первым делом сменим пользователя, чтоб не заходить под root, определим способ входа по SSH, настроим firewall (UFW) и ограничения для подключения по IP (fail2ban).
5. Пользователи и sudo
Для чего нужен отдельный пользователь и почему не использовать root: Коротко: дело не в том, что sudo-пользователь «слабее» root. Он даёт несколько слоёв защиты и прозрачность, которых у прямого root нет.
Зачем это нужно:
- Закрыть root по SSH. root есть на любой Linux-машине, поэтому ботам остаётся подобрать только ключ или пароль.
- Пароль sudo как второй фактор. Если утёк приватный ключ, атакующий попадёт на сервер, но без пароля пользователя root не станет. С прямым root-доступом утечка ключа сразу означает полный контроль над сервером.
- Защита от собственных ошибок. По умолчанию ты работаешь без привилегий, а повышаешь их явно и точечно.
rm -rf /var/www/ myapp— лишний пробел, и вместо/var/www/myappудаляется весь/var/www. От обычного пользователя команда упрётся в права, а от root выполнится. - Аудит и ответственность. Каждый вызов sudo пишется в
/var/log/auth.log(journalctl _COMM=sudo): кто, когда и какую команду выполнил. Если на сервер ходят несколько человек под root, в логах будет только «root что-то сделал». - Управление доступом. У каждого человека свой пользователь и свой ключ. Когда кто-то уходит, достаточно
userdelили удалить ключ из егоauthorized_keys. Не нужно перевыпускать общий root-ключ для всех. - Разделение прав. Через
sudoersможно выдать не полный root, а конкретные команды. Например, CI-пользователю толькоsystemctl restart myapp.
5.1. Создаем пользователя
adduser demo
Создает пользователя demo (интерактивно, с домашней папкой).
После команды попросит ввести пароль (надо придумать сложный пароль или сгенерировать его, главное — не забыть). Принцип тот же, что и при входе: вводимые символы не выводятся.
Подтверди введенный пароль.
Далее идет настройка пользователя, ее можно не заполнять.
У меня получилось что-то такое:
~# adduser demo
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for demo
Enter the new value, or press ENTER for the default
Full Name []: demo
Room Number []: 1
Work Phone []: 123
Home Phone []: 123
Other []: other
5.2. Добавляем пользователя в sudo
Сам по себе созданный пользователь demo — обычный непривилегированный аккаунт, команда sudo для него не работает. Добавление в группу sudo и есть то, что даёт ему право повышать привилегии.
usermod -aG sudo demo
Флаги
-G sudoзадаёт список дополнительных групп;-a(append) добавляет к существующим. Без-aпользователь будет удалён из всех остальных дополнительных групп (docker,admи т.д.). Это частая ошибка.
Нюансы
- Членство применяется при новом логине: перезайди по SSH или выполни
newgrp sudo. Проверка:groupsилиid demo. - Проверить, что именно разрешено:
sudo -l -U demo.
5.3. Проверяем, что все правильно применилось
Проверка, что пользователь находится в тех группах, в которых мы хотели, и ничего не упущено.
id demo # проверить группы
id demo
- ✅
uid=1000(demo) gid=1000(demo) groups=1000(demo),27(sudo),100(users)— есть27(sudo) - ❌ Нет
sudoв списке → повторитьusermod -aG sudo demo
💡 Членство в группах применяется при создании сессии — перезайти после изменения.
Теперь можем перезайти в новую сессию:
Выход из сессии:
exit
Вход с новым пользователем:
ssh demo@<IP-сервера>
Вводим пароль от пользователя demo, которого создали.
И если все правильно сделано, то теперь старт строки в консоли будет:
demo@DemoServer:~$
demo — имя пользователя, которого создавали.
DemoServer — имя хоста, на которое поменяли выше.
6. SSH-ключи
⚠️ Дальше на сервере работаем от demo, без sudo: всё делается в его домашней папке, и файлы сразу принадлежат ему. Если создать .ssh от root или через sudo, владельцем будет root, и sshd проигнорирует ключ. Рабочую сессию держим открытой, пока не убедимся, что вход по ключу работает в соседнем окне.
Настроим подключение через SSH-ключ. Для чего это нужно? Коротко, главная разница: пароль — это общий секрет, который ты отправляешь серверу, а приватный ключ никогда не покидает твою машину.
Так чем же они лучше по отношению к login/pass:
- Секрет не передаётся. С паролем сервер получает его в открытом виде (внутри зашифрованного канала) и сравнивает с хешем. Если сервер скомпрометирован или ты подключился к поддельному хосту (проигнорировал предупреждение про host key), пароль утёк. С ключом сервер присылает challenge, ты подписываешь его приватным ключом, сервер проверяет подпись публичным ключом.
- Брутфорс бесполезен. Обычный пароль — несколько десятков бит энтропии, и его можно подбирать онлайн или по словарю. Ключ на машине — это порядка 128 бит стойкости, перебор невозможен в принципе. Боты, которые сейчас долбят твой порт 22, просто бессмысленны, когда
PasswordAuthentication no(настраивать будем дальше). - Нет повторного использования. Пароли люди переиспользуют, и утечка с одного сайта открывает сервер. Ключ уникален и не вводится руками, поэтому его нельзя «подсмотреть» или ввести на фишинговой странице.
- Утёкший публичный ключ ничего не даёт.
authorized_keysна сервере не секрет. Утёкшая база паролей (даже хешей) — серьёзная проблема. - Автоматизация. CI/CD, Ansible, git push, scp в скриптах. С паролями пришлось бы хранить их в открытом виде где-то в скрипте или переменных. Ключ можно выдать отдельный, с ограничениями прямо в
authorized_keys(command=, from=, no-port-forwarding). - Управление доступом. Один ключ на человека или устройство. Ноутбук потерян — удаляешь одну строку из
authorized_keys, остальные доступы не затронуты. Пароль пришлось бы менять для всех.
Есть нюансы, которые надо учитывать. Приватный ключ лежит файлом на диске. Если его украдут вместе с ноутбуком или через малварь, он даёт доступ. Защита:
Passphraseна ключ. Тогда украденный файл без фразы бесполезен. Получается два фактора: «что у тебя есть» (файл) + «что ты знаешь» (фраза). Вводить её постоянно не придётся:ssh-agentдержит расшифрованный ключ в памяти на сессию.- Аппаратный ключ (YubiKey и аналоги, тип ed25519-sk): приватный ключ вообще не извлекаем из устройства, нужно физическое касание.
6.1. Создание пары ключей
На локальной машине:
Проверяем папку хранения ключей: может быть пусто, а могут быть другие ключи.
dir $env:USERPROFILE\.ssh
Непосредственно генерация ключей.
ssh-keygen -t ed25519 -C "demo@DemoServer" -f $env:USERPROFILE\.ssh\id_demo
demo@DemoServer — пользователь и сервер, для которого создаются ключи.
id_demo — название ключей.
Попросит ввести пароль для ключа (можно использовать без пароля, но только если делаешь для тестов, иначе это небезопасно).
По итогу будет создана пара ключей, посмотреть: dir $env:USERPROFILE\.ssh
-a--- 08.10.2026 14:03 464 id_demo
-a--- 08.10.2026 14:03 98 id_demo.pub <- вот этот ключ пойдет на сервер.
Далее, чтобы не копировать ручками данные ключа:
Get-Content $env:USERPROFILE\.ssh\id_demo.pub | Set-Clipboard
⚠️ Приватный ключ (без .pub) никогда не покидает твою машину. Генерировать на сервере — нельзя.
6.2. Перенос публичного ключа на сервер
На сервере:
mkdir -p /home/demo/.ssh && chmod 700 /home/demo/.ssh
mkdir -p — создает папку. -p — создает родительские папки и не падает, если папка уже создана.
chmod 700 — права на папку: читать, писать и заходить может только владелец.
Вставляем публичный ключ из буфера в файл.
echo "ssh-ed25519 AAAA... comment" >> /home/demo/.ssh/authorized_keys
Команда записывает в файл твой ключ, который скопирован Get-Content $env:USERPROFILE\.ssh\id_demo.pub | Set-Clipboard на локальной машине.
ssh-ed25519 AAAA... — тело ключа.
Далее проставляем владельца. Если всё делали от demo, шаг не нужен: владелец уже правильный. Если .ssh создавали от root, отдаём папку пользователю (менять владельца может только root, поэтому sudo):
sudo chown -R demo:demo /home/demo/.ssh
Разбор
chown— меняет владельца;demo:demo— формат пользователь:группа. Первое — владелец, второе — группа (приadduserсоздаётся одноимённая группа). Можно писатьdemo:с двоеточием, тогда подставится основная группа пользователя;-R— рекурсивно: каталог и всё внутри (authorized_keysи т.д.). Без-Rсменится владелец только у самого.ssh, аauthorized_keysостанется root-овым.
Далее выставляем права на файл с ключом:
chmod 600 /home/demo/.ssh/authorized_keys
Почему не 700: потому что он не исполняемый. Можно использовать 400, чтоб защитить от случайного изменения.
Обязательные права (иначе sshd молча игнорирует ключ):
| Путь | Права | Владелец |
|---|---|---|
~/.ssh | 700 drwx------ | сам пользователь |
~/.ssh/authorized_keys | 600 -rw------- | сам пользователь |
Проверяем права: ls -la /home/demo/.ssh. Должны получить:
drwx------ 2 demo demo 4096 Oct 8 11:22 .
drwxr-x--- 4 demo demo 4096 Oct 8 11:40 ..
-rw------- 1 demo demo 105 Oct 8 11:22 authorized_keys
И проверим правильность заполнения файла:
wc -l /home/demo/.ssh/authorized_keys # должно быть ровно 1
wc -l authorized_keys
- ✅
1— ключ целый - ❌
2+— ключ разорвался при вставке → очистить файл и повторить
После всего сделанного можно проверить доступ в отдельной сессии (новое окно терминала):
ssh -i $env:USERPROFILE\.ssh\id_demo -o IdentitiesOnly=yes demo@<IP-сервера>
Что делает каждая часть:
-i <путь>указывает, какой приватный ключ использовать.-o IdentitiesOnly=yesзаставляет ssh предлагать серверу только этот ключ, а не все ключи изssh-agentи~/.sshподряд. Без этой опции при нескольких ключах можно упереться вToo many authentication failures: sshd по умолчанию допускает 6 попыток (MaxAuthTries), а каждый лишний ключ съедает одну. Опция-oвообще позволяет передать любой параметр конфига прямо в командной строке.demo@<IP-сервера>задаёт пользователя и хост. Вместоuser@hostможно писать-l demo <IP-сервера>.
Если все правильно настроено, то после ввода passphrase for key ты зайдешь на сервер.
chmod — права доступа к файлам и каталогам
chmod меняет права (кто что может делать с файлом/каталогом). Владельца не меняет — для этого chown.
Права задаются для трёх категорий:
u— владелец (user)g— группа (group)o— остальные (others)
1. Что значат r / w / x
| Бит | Число | Для файла | Для каталога |
|---|---|---|---|
r | 4 | читать содержимое | видеть список файлов (ls) |
w | 2 | изменять содержимое | создавать/удалять файлы внутри |
x | 1 | запускать как программу | проходить внутрь (cd, открыть файл по имени) |
Для каталога без
xостальные права почти бесполезны — внутрь не попасть. Для файлаxнужен только скриптам и бинарникам. Скрипт можно запустить и безxчерез интерпретатор:bash script.sh.
2. Числовая запись
Каждая цифра — сумма битов r=4 + w=2 + x=1, порядок: владелец / группа / остальные.
chmod 755 dir # rwxr-xr-x — все заходят и читают, пишет только владелец
chmod 700 dir # rwx------ — только владелец
chmod 644 file # rw-r--r-- — все читают, пишет только владелец
chmod 600 file # rw------- — только владелец читает и пишет
chmod 400 file # r-------- — только чтение владельцем
3. Символьная запись — точечно меняет биты, не трогая остальные
chmod +x deploy.sh # добавить x всем
chmod u+x deploy.sh # только владельцу
chmod go-rwx secret # убрать всё у группы и остальных
chmod g=r file # группе ровно r
+ добавить, - убрать, = выставить ровно.
4. Рекурсивно
chmod -R 755 /var/www # ❌ файлы тоже получат x
Правильно — отдельно каталоги и файлы:
find /var/www -type d -exec chmod 755 {} +
find /var/www -type f -exec chmod 644 {} +
Или через X (ставит x только каталогам и уже исполняемым файлам):
chmod -R u=rwX,go=rX /var/www
5. Кто может менять права
Только владелец файла или root. В отличие от chown, свои файлы можно менять без sudo.
6. Пример: права для SSH
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
sshd (StrictModes yes) молча игнорирует ключи, если .ssh или authorized_keys доступны на запись группе/остальным → Permission denied (publickey).
Причина видна в логе: sudo journalctl -u ssh -n 50.
Шпаргалка
| Что | Права |
|---|---|
| каталог общий / приватный | 755 / 700 |
| файл общий / приватный | 644 / 600 |
| скрипт, бинарник | 755 / 700 |
приватный ключ, authorized_keys | 600 (400) |
Проверить права: ls -la (для каталога: ls -ld dir).
6.3. SSH config на клиенте
Каждый раз вводить команду с указанием ключа и пользователя слишком много. Поэтому можно создать алиас для команды, при вводе которой будет подтягиваться нужный ключ на нужный сервер.
Создадим файл на локальной машине
Для начала надо проверить, что файл еще не создан; если файл создан, то использовать вариант Б.
dir $env:USERPROFILE\.ssh
Вариант А (файла нет в списке):
C:\Users\USER\.ssh\config (без расширения!):
Host demo
HostName <IP-сервера>
User demo
IdentityFile ~/.ssh/id_demo
IdentitiesOnly yes
ServerAliveInterval 60
Создать надёжно (Notepad дописывает .txt и может записать UTF-16 — у меня так и было):
Откуда берутся .txt и UTF-16
.txt— Notepad при «Сохранить как» сам дописывает расширение, и файл получаетсяconfig.txt. ssh ищет файл строгоconfigбез расширения.- UTF-16 — у меня при настройке личного сервера файл оказался в UTF-16, и ssh его не прочитал. В UTF-16 файл пишут:
- Notepad, если в окне сохранения выбрана кодировка
UTF-16 LE/UTF-16 BE(в Windows 10 1903+ и Windows 11 по умолчанию стоитUTF-8; в старых версиях UTF-16 LE называлсяUnicode); - перенаправление
>иOut-Fileв Windows PowerShell 5.1:echo "..." > configсоздаёт файл в UTF-16 LE.
- Notepad, если в окне сохранения выбрана кодировка
- Проверить кодировку: открыть файл в Notepad — она видна в правом нижнем углу строки состояния.
Поэтому надёжнее создавать файл командой ниже: Set-Content -Encoding ascii пишет его без BOM и без UTF-16, а имя задаётся точно.
@"
Host demo
HostName <IP-сервера>
User demo
IdentityFile ~/.ssh/id_demo
IdentitiesOnly yes
ServerAliveInterval 60
"@ | Set-Content -Path $env:USERPROFILE\.ssh\config -Encoding ascii
- ❌
Could not resolve hostname demo→ конфига нет, лежит какconfig.txt, или кодировка UTF-16 - 💡
IdentitiesOnly yesобязателен при нескольких ключах — иначе ssh переберёт все и упрётся вMaxAuthTries - 💡
ServerAliveInterval 60— клиент раз в 60 секунд шлёт серверу пустой пакет, и простаивающую сессию не разрывает роутер или NAT
Вариант Б (файл есть в списке):
Запишем в переменную расположение файла настроек:
$cfg = "$env:USERPROFILE\.ssh\config"
Сделаем бэкап на всякий случай, чтоб не потерять данные, которые были уже записаны:
Copy-Item $cfg "$cfg.bak" -ErrorAction SilentlyContinue
И добавим запись о нашем сервере в файл:
@"
Host demo
HostName <IP-сервера>
User demo
IdentityFile ~/.ssh/id_demo
IdentitiesOnly yes
ServerAliveInterval 60
"@ | Add-Content -Path $cfg -Encoding ascii
Проверить файл:
Get-Content $env:USERPROFILE\.ssh\config
Должны получить:
Host ...
... если были предыдущие записи...
Host demo
HostName <IP-сервера>
User demo
IdentityFile ~/.ssh/id_demo
IdentitiesOnly yes
ServerAliveInterval 60
И теперь можем проверить вход по алиасу:
ssh demo
Если все правильно настроено, то попросит ввести passphrase for key, и после ввода доступ на сервер будет разрешен.
Чтоб не вводить постоянно пароль от ключа:
Для этого добавляем ключ в ssh-agent:
Запускаем PowerShell от администратора. Проверяем статус агента:
Get-Service ssh-agent | Select-Object Status, StartType
Ответ должен быть:
Status StartType
------ ---------
Running Automatic
Если StartType — Disabled/Manual, то вызываем:
Set-Service ssh-agent -StartupType Automatic
Если Status — Stopped, то вызываем:
Start-Service ssh-agent
После надо добавить наш ключ в агент:
ssh-add $env:USERPROFILE\.ssh\id_demo
Попросит ввести пароль ключа и добавит агенту этот ключ.
И теперь ssh demo позволит войти без ввода пароля каждый раз. Это поведение сохранится после перезагрузки компьютера.
Если необходимо убрать ключи из агента:
ssh-add -d $env:USERPROFILE\.ssh\id_demo # один ключ
ssh-add -D # все ключи
7. Харденинг SSH
⚠️ Правило этапа: старую сессию не закрывать, пока новый вход не проверен во втором окне.
💡 С этого раздела и до конца статьи работаем от demo: правим системные файлы и службы, поэтому команды идут с sudo.
На этом этапе будет закрываться вход по логину и паролю, и как итог останется вход только по SSH-ключу, который был настроен на предыдущем этапе. Поэтому правило, указанное выше, важно для работы. В любом случае останется лазейка для доступа через консоль провайдера, но лучше этого избежать.
7.1. Проверка настроек SSH-подключения
Довольно часто у провайдеров есть дополнительные файлы настроек сервера,
поэтому надо проверить, не подключается ли дополнительный файл. Обычно он подключается в самом начале файла: Include /.....
grep -vE '^\s*#|^\s*$' /etc/ssh/sshd_config
У меня получилось, что есть дополнительный файл настроек. Так как он подключен первым, то значения читаются сначала из него:
Include /etc/ssh/sshd_config.d/*.conf
PermitRootLogin yes
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_* COLORTERM NO_COLOR
Subsystem sftp /usr/lib/openssh/sftp-server
Как видно, подключен Include /etc/ssh/sshd_config.d/*.conf, значит, проверяем файлы в этой директории:
ls -la /etc/ssh/sshd_config.d/ # ЛОВУШКА: смотреть обязательно
sudo cat /etc/ssh/sshd_config.d/*.conf
В выводе последней команды получаем PasswordAuthentication yes.
⚠️ Include стоит первой строкой, а побеждает ПЕРВОЕ встреченное значение директивы. Файлы из sshd_config.d/ перебивают основной конфиг. Cloud-init часто кладёт туда PasswordAuthentication yes.
Что мы бы получили при изменении только основного файла конфигурации: мы бы там отключили авторизацию по паролю, а вход по login/password тихо остался бы разрешен.
Поэтому комментируем эту строку в 50-cloud-init.conf, чтобы действовали только наши настройки.
sudo sed -i 's/^PasswordAuthentication/#PasswordAuthentication/' /etc/ssh/sshd_config.d/50-cloud-init.conf
Команда заменяет подстроку на строку. В нашем случае это только одна строка.
Можно изменить через sudo nano или любой другой редактор.
7.2. Свой конфиг
И теперь создадим свои настройки для входа на сервер. Команда одна и для demo, и для root (от root sudo просто ничего не меняет):
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null << 'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
💡 Префикс 10- выбран специально: файлы из sshd_config.d/ читаются по алфавиту, и побеждает первое значение. 10-hardening.conf читается раньше 50-cloud-init.conf, поэтому наши настройки победят, даже если cloud-init когда-нибудь вернёт PasswordAuthentication yes. С префиксом 99- всё держалось бы только на закомментированной строке в 50-cloud-init.conf.
⚠️ Почему tee, а не sudo cat > файл << 'EOF': sudo применяется только к команде cat, а перенаправление > выполняет оболочка пользователя demo без прав root — получим Permission denied. tee пишет в файл сам, поэтому запись идёт от root. > /dev/null убирает дублирование текста конфига в консоль.
Далее перед перезапуском проверяем правильность конфига.
sudo sshd -t && echo "CONFIG OK" # ПРОВЕРКА ДО ПРИМЕНЕНИЯ
Должно вернуть CONFIG OK.
sshd -t
- ✅
CONFIG OK - ❌
line N: Bad configuration option→ чинить строку, не перезапускать sshd
И перечитываем конфиг службы.
sudo systemctl reload ssh # reload, не restart
7.3. Проверка входа
Контрольная проверка из нового окна:
- ✅
ssh root@<IP-сервера>→Permission denied (publickey)— root закрыт - ✅
ssh demo→ пускает - ❌ Спрашивает пароль →
PasswordAuthenticationперебит файлом изsshd_config.d/
Если при
ssh root@<IP-сервера>видимReceived disconnect from <IP-сервера> port 22:2: Too many authentication failures, это не ошибка: ssh перебирает ключи из.sshна локальной машине, а лимит у нас 3 попытки на сессию. Проверяем без ключей, только паролем:ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password root@<IP-сервера> ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password demo@<IP-сервера>
- ✅ Обе сразу отвечают
Permission denied (publickey), пароль не спрашивают: вход по паролю закрыт для всех- ❌ Спрашивает пароль →
PasswordAuthenticationперебит файлом изsshd_config.d/
💡 Если сломал SSH — аварийный вход через VNC-консоль в панели провайдера под root.
8. Firewall (UFW)
Любой сервер, имеющий публичный IP, подвергается атакам: боты перебирают порты, ищут открытый SSH, забытую базу данных или dev-сервер, который по недосмотру слушает все интерфейсы. Первая линия обороны здесь — сетевой фильтр, встроенный прямо в ядро Linux. UFW (Uncomplicated Firewall) управляет открытостью сервера. Это простая надстройка, которая превращает «разрешить SSH и веб, остальное закрыть» в пару понятных команд, а всю низкоуровневую работу берёт на себя. Принцип простой: по умолчанию всё входящее запрещено, открыто только то, что ты разрешил явно. И в этом пункте будем настраивать UFW.
Для начала надо проверить, в каком статусе находится UFW.
sudo ufw status verbose # состояние + политики (только когда active)
Если будет ругаться на то, что нет UFW, значит, надо его установить.
sudo apt install ufw
После установки снова проверим, что всё установилось: ufw status verbose должен показать Status: inactive. Затем проверим, нет ли уже добавленных правил:
sudo ufw show added # добавленные правила (работает и когда inactive)
- ✅
(None)— правил нет, начинаем с чистого листа - ⚠️ Есть строки
ufw allow …— правила уже кто-то добавил (провайдер или ты раньше), просмотри их перед включением
💡 ufw status numbered при выключенном UFW правил не показывает, только Status: inactive. Она пригодится ниже, чтобы удалять правила по номеру.
Сначала узнаём порт SSH. 22 — стандартный, но провайдер или ты могли его поменять:
sudo sshd -T | grep -i '^port'
В выводе будет port <номер порта>, его и подставляем в разрешённые.
⚠️ Порядок критичен: сначала allow, потом enable. Наоборот — мгновенный разрыв сессии, и придётся идти в консоль провайдера.
sudo ufw allow 22/tcp
ufw allow 22/tcp
- ✅ Две строки:
Rules updated+Rules updated (v6)— покрыты IPv4 и IPv6 - ⚠️ Одна строка → IPv6 не покрыт, сервер доступен по v6 при «закрытом» v4
Включаем firewall:
sudo ufw enable
UFW спросит Command may disrupt existing ssh connections. Proceed with operation (y|n)?. Это стандартное предупреждение: если порт SSH уже разрешён (шаг выше), отвечаем y, сессия не оборвётся. Ответ: Firewall is active and enabled on system startup.
И проверяем статус первой командой из этого пункта, ufw status verbose. Вывод должен быть:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
Если надо удалить правило, то можно запросить все разрешения при запущенном firewall (ufw status numbered) и удалить правило командой, подставив нужный номер:
sudo ufw delete N
ufw status verbose
- ✅
Status: active·Default: deny (incoming), allow (outgoing)· правило 22/tcp для v4 и v6
Docker создает свои правила в обход UFW
Реальные правила ядра (UFW показывает не всё):
sudo nft list ruleset | head -30
- Счётчики
counter packets N— если нули, до правила не доходит, срабатывает более раннее - ⚠️
table ip filter is managed by iptables-nft, do not touch— правь только черезufw
🔴 Docker пишет правила в обход UFW. -p 3000:3000 открывает порт в интернет, хотя ufw status показывает закрыто. Проверять только через nft list ruleset.
Docker и UFW — отдельная тема, в эту шпаргалку не входит.
9. fail2ban
После включения firewall попытки подключения не закончатся, боты будут перебирать логин, пароль, порты и пытаться всячески получить доступ к твоему серверу. Поэтому надо ограничить такие подключения. С этими задачами справляется fail2ban. Он блокирует IP-адреса после неверных попыток подключения к серверу. Можно настроить количество повторов при отвергнутом подключении и градацию банов в зависимости от повторений.
Проверить журналы подключения к серверу
1. Попытки входа по SSH
Все попытки аутентификации, удачные и неудачные, записываются в журнал sshd:
sudo journalctl -u ssh --since "1 hour ago" # всё по SSH за последний час
sudo journalctl -u ssh -f # смотреть в реальном времени
Классический текстовый лог, если установлен rsyslog (на Ubuntu он есть по умолчанию):
sudo grep -E 'Failed password|Invalid user|authenticating user' /var/log/auth.log | tail -20
💡 После харденинга (раздел 7) строк Failed password уже не будет: пароль не принимается. Боты оставляют Invalid user ЛОГИН from IP (такого логина нет) и Connection closed by authenticating user ЛОГИН IP … [preauth] (логин есть, ключ не подошёл).
Топ IP по неудачным попыткам — самая полезная выжимка:
sudo journalctl -u ssh --since today \
| grep -oP '(Failed password for \S+ from|Invalid user \S+ from|closed by authenticating user \S+) \K[\d.]+' \
| sort | uniq -c | sort -rn | head -20
Какие логины перебирают (обычно это root, admin, ubuntu, test, postgres):
sudo journalctl -u ssh --since today | grep -oP 'Invalid user \K\S+' | sort | uniq -c | sort -rn | head
2. Успешные входы
💡 В Ubuntu 26.04 команд last, lastb, lastlog по умолчанию нет: старые журналы wtmp, btmp и lastlog заменены. Ставим (оба пакета из universe):
sudo apt install wtmpdb lastlog2
last -a | head -20 # успешные входы с IP
lastlog2 # последний вход каждого пользователя
lastb больше нет: неудачные попытки смотрим через journalctl (пункт 1).
last стоит проверять регулярно: здесь важны не попытки, а успешные входы с незнакомых IP.
3. Кто подключён прямо сейчас
who # залогиненные пользователи
sudo ss -tnp state established '( sport = :22 )' # активные TCP-сессии на порт SSH
Порт 22 замени на свой SSH-порт.
4. Что режет UFW (все порты, не только SSH)
По умолчанию UFW логирует отброшенные пакеты на уровне low. Проверить и при необходимости включить:
sudo ufw status verbose | grep -i logging
sudo ufw logging low
Смотреть журнал:
sudo grep 'UFW BLOCK' /var/log/ufw.log | tail -20
# или через журнал ядра
sudo journalctl -k --since "1 hour ago" | grep 'UFW BLOCK'
Топ IP и портов, которые простукивают:
sudo grep 'UFW BLOCK' /var/log/ufw.log | grep -oP 'SRC=\K[\d.]+' | sort | uniq -c | sort -rn | head
sudo grep 'UFW BLOCK' /var/log/ufw.log | grep -oP 'DPT=\K\d+' | sort | uniq -c | sort -rn | head
Второй список обычно наглядно показывает, что ищут боты: 22, 23, 3389, 5432, 6379, 8080 и так далее.
Установим пакет:
sudo apt install fail2ban -y
Проверим, работает ли сервис:
sudo fail2ban-client status # список тюрем
fail2ban-client status
- ✅
Number of jail: 1·Jail list: sshd - ❌
Number of jail: 0→ сервис запущен, но ни одна тюрьма не включена. Сразу после установки проверить/etc/fail2ban/jail.d/defaults-debian.conf(там должно быть[sshd]иenabled = true), после настройки ниже —jail.local
Можно посмотреть статистику только по одной тюрьме:
sudo fail2ban-client status sshd # статистика тюрьмы
У меня получается вот так: как видно, сервис уже работает и считает количество неудачных подключений.
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 753
| `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 31
`- Banned IP list:
Проверить длительность бана в секундах — на сколько блокируется адрес после исчерпания попыток:
sudo fail2ban-client get sshd bantime # ФАКТИЧЕСКОЕ значение
И если надо вернуть адрес из бана:
sudo fail2ban-client set sshd unbanip <IP> # разбанить
Далее настроим свой конфиг, который переживёт перезапуск сервиса и обновление пакета.
Конфиг — jail.local, не jail.conf (перезаписывается при обновлении пакета):
sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w
[sshd]
enabled = true
EOF
| параметр | значение | смысл |
|---|---|---|
findtime = 10m | окно 10 минут | Скользящее окно, в котором считаются неудачные попытки |
maxretry = 3 | 3 попытки | Третья неудача в окне findtime приводит к бану IP |
bantime = 1h | 1 час | Длительность первого бана |
bantime.increment = true | вкл. | Повторные баны того же IP становятся длиннее (fail2ban помнит историю в своей SQLite-БД) |
bantime.factor = 2 | ×2 | Постоянный множитель для повторных банов. Удвоение на каждый повтор даёт сама формула (bantime × 2^число_банов × factor), а factor=2 делает каждый повторный бан ещё вдвое длиннее: 1 ч → 4 ч → 8 ч → 16 ч… до maxtime |
bantime.maxtime = 1w | неделя | Потолок: дольше недели бан не растёт |
Проверим конфиг перед запуском:
sudo fail2ban-client -t
Должны увидеть ответ OK: configuration test is successful
И перезапустим сервис:
sudo systemctl restart fail2ban
- ⚠️
Failed to access socket path ... Is fail2ban running?сразу после restart → не ошибка, демон на Python стартует пару секунд. Подождать и повторить. Проверитьsystemctl status fail2ban→Server ready - ✅
fail2ban-client get sshd bantime→3600(значитjail.localподхватился, а не дефолт600)
💡 Баны хранятся в SQLite и переживают рестарт сервиса.
Итог: чеклист
-
apt upgradeвыполнен,reboot-requiredнет (ls -l /var/run/reboot-required) - Время синхронизировано (
timedatectl) - Автообновления включены (
cat /etc/apt/apt.conf.d/20auto-upgrades) - Swap активен,
swappiness = 10(swapon --show,cat /proc/sys/vm/swappiness) - Имя хоста сменено и разрешается мгновенно (
hostname -f) -
demoв группеsudo(id demo→ есть27(sudo)) - Вход под
demoпо ключу, root и пароль закрыты (sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication') - UFW активен, открыт только порт SSH (
sudo ufw status verbose) - fail2ban: тюрьма
sshdработает,bantime= 3600 (sudo fail2ban-client get sshd bantime)