Илья Овсенев
← Все статьи

Содержание

  1. Исходные данные
  2. Доступ к серверу
  3. 1. Разведка системы
  4. 2. База системы
  5. 3. Автообновления
  6. 4. Swap
  7. Настройки безопасности сервера
  8. 5. Пользователи и sudo
  9. 6. SSH-ключи
  10. 7. Харденинг SSH
  11. 8. Firewall (UFW)
  12. 9. fail2ban
  13. Итог: чеклист

Первичная настройка и защита сервера Ubuntu 26.04

Шпаргалка по первичной настройке свежего облачного сервера на 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 нет.

Зачем это нужно:

  1. Закрыть root по SSH. root есть на любой Linux-машине, поэтому ботам остаётся подобрать только ключ или пароль.
  2. Пароль sudo как второй фактор. Если утёк приватный ключ, атакующий попадёт на сервер, но без пароля пользователя root не станет. С прямым root-доступом утечка ключа сразу означает полный контроль над сервером.
  3. Защита от собственных ошибок. По умолчанию ты работаешь без привилегий, а повышаешь их явно и точечно. rm -rf /var/www/ myapp — лишний пробел, и вместо /var/www/myapp удаляется весь /var/www. От обычного пользователя команда упрётся в права, а от root выполнится.
  4. Аудит и ответственность. Каждый вызов sudo пишется в /var/log/auth.log (journalctl _COMM=sudo): кто, когда и какую команду выполнил. Если на сервер ходят несколько человек под root, в логах будет только «root что-то сделал».
  5. Управление доступом. У каждого человека свой пользователь и свой ключ. Когда кто-то уходит, достаточно userdel или удалить ключ из его authorized_keys. Не нужно перевыпускать общий root-ключ для всех.
  6. Разделение прав. Через 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:

  1. Секрет не передаётся. С паролем сервер получает его в открытом виде (внутри зашифрованного канала) и сравнивает с хешем. Если сервер скомпрометирован или ты подключился к поддельному хосту (проигнорировал предупреждение про host key), пароль утёк. С ключом сервер присылает challenge, ты подписываешь его приватным ключом, сервер проверяет подпись публичным ключом.
  2. Брутфорс бесполезен. Обычный пароль — несколько десятков бит энтропии, и его можно подбирать онлайн или по словарю. Ключ на машине — это порядка 128 бит стойкости, перебор невозможен в принципе. Боты, которые сейчас долбят твой порт 22, просто бессмысленны, когда PasswordAuthentication no (настраивать будем дальше).
  3. Нет повторного использования. Пароли люди переиспользуют, и утечка с одного сайта открывает сервер. Ключ уникален и не вводится руками, поэтому его нельзя «подсмотреть» или ввести на фишинговой странице.
  4. Утёкший публичный ключ ничего не даёт. authorized_keys на сервере не секрет. Утёкшая база паролей (даже хешей) — серьёзная проблема.
  5. Автоматизация. CI/CD, Ansible, git push, scp в скриптах. С паролями пришлось бы хранить их в открытом виде где-то в скрипте или переменных. Ключ можно выдать отдельный, с ограничениями прямо в authorized_keys (command=, from=, no-port-forwarding).
  6. Управление доступом. Один ключ на человека или устройство. Ноутбук потерян — удаляешь одну строку из 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 молча игнорирует ключ):

ПутьПраваВладелец
~/.ssh700 drwx------сам пользователь
~/.ssh/authorized_keys600 -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

БитЧислоДля файлаДля каталога
r4читать содержимоевидеть список файлов (ls)
w2изменять содержимоесоздавать/удалять файлы внутри
x1запускать как программупроходить внутрь (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_keys600 (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 — она видна в правом нижнем углу строки состояния.

Поэтому надёжнее создавать файл командой ниже: 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 = 33 попыткиТретья неудача в окне findtime приводит к бану IP
bantime = 1h1 часДлительность первого бана
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)