Это первая часть гайда-памятки по настройке self-hosted-инфраструктуры. Гайд написан под Ubuntu 24.04 LTS на DigitalOcean. На других дистрибутивах базовая логика та же, отличаться будут только команды и расположение конфигов. Большинство разделов независимы и опциональны: набор собран под мои задачи, берите нужное. Разделы 1-2 (юзер и SSH) стоит сделать первыми и целиком.
Содержание:
- Настройка non-root юзера и привязка ssh-ключа
- Запрет root-логина и паролей по SSH
- Файрвол UFW
- fail2ban от брутфорса
- Автообновления безопасности
- Базовые утилиты
- Часовой пояс и hostname
- Выделение swap
- Git, Docker + Docker Compose
- DigitalOcean Metrics Agent
- Resource Alerts в DO
- SSH-конфиг на локальной машине
- Бэкапы DO
- Лимит логов journald
Чеклист
Безопасность
- Создан non-root пользователь с sudo
- SSH-ключ скопирован non-root юзеру, проверен вход
-
SSH настроен через
/etc/ssh/sshd_config.d/99-ssh.conf(root отключен, только ключ,AllowUsers, таймауты) - UFW активен, открыты только 22/80/443
- fail2ban установлен и активен
- unattended-upgrades включены
Базовая система
-
apt upgradeвыполнен - Часовой пояс и hostname заданы
- Swap создан
- Установлены: curl, wget, htop, ncdu, tmux, tree
-
journald ограничен 500M через
/etc/systemd/journald.conf.d/00-server.conf
Инфраструктура
- Git, Docker + Docker compose установлены
- Текущий пользователь в группе docker
- do-agent установлен и работает
- DO Backups включены
- Resource Alerts настроены
- Billing Alert настроен
На локальной машине
-
~/.ssh/configс алиасом -
Алиас работает (
ssh vpsподключает)
Главное
Минимальные привилегии. Каждый сервис и каждый юзер должны иметь минимум прав. Не запускаем ничего от root, не открываем ненужные порты, не даем sudo юзеру, которому хватит непривилегированного шелла.
Минимизация поверхности атаки. Базовое правило: deny по дефолту, разрешать точечно. VPS должен снаружи отвечать только на 22, 80 и 443, а все остальное - слушать на 127.0.0.1 либо проксироваться.
1. Настройка non-root юзера и привязка ssh-ключа
На VPS создаем отдельного пользователя username для повседневной работы (замени username на свое имя). Команды выполняем под root на свежем дроплете. Дальше мы копируем ssh-ключ из /root/.ssh/authorized_keys - он там есть, только если при создании дроплета был выбран SSH-ключ. Проверяем, что файл не пустой:
cat ~/.ssh/authorized_keys # должен показать твой публичный ключ
Если пусто (дроплет создан с паролем) - сначала заливаем ключ с локальной машины (ssh-copy-id username@<IP_VPS>, пока пароль еще разрешен), иначе после раздела 2 (отключение паролей) потеряете доступ.
adduser username
usermod -aG sudo username # даем права sudo
mkdir -p /home/username/.ssh
cp ~/.ssh/authorized_keys /home/username/.ssh/
chown -R username:username /home/username/.ssh
chmod 700 /home/username/.ssh
chmod 600 /home/username/.ssh/authorized_keys
- Переносим публичный ключ в
~/.sshнового юзера, чтобы можно было заходить по ключу под ним. - Ставим владельцем файлов нового юзера.
- Права 700 на
~/.ssh(rwx владельцу, остальным ничего) и 600 наauthorized_keys(rw владельцу) - требование sshd, иначе он проигнорирует ключи.
2. Настройка SSH
Отключаем небезопасные способы входа, ограничиваем число попыток и убиваем зависшие сессии. Дефолтный /etc/ssh/sshd_config напрямую не трогаем - при обновлении пакета openssh-server apt спросит про конфликт, и можно случайно вернуть PermitRootLogin yes. Кладем свои настройки отдельным файлом в drop-in каталог (отдельные .conf-файлы, которые подхватываются в дополнение к основному конфигу):
sudo nano /etc/ssh/sshd_config.d/99-ssh.confPermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers username
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowAgentForwarding no
Что делает каждая строка:
PermitRootLogin no- не пускать под root, заходим под non-root и поднимаем права через sudo.PasswordAuthentication no- не пускать под паролем, только ключ.KbdInteractiveAuthentication no- отключает интерактивные PAM-диалоги. Без нееPasswordAuthentication noне закрывает все способы спросить пароль.PubkeyAuthentication yes- включаем сам метод, разрешаем логин по ключу.AuthenticationMethods publickey- требуем, чтобы для входа был пройден именно publickey и только он (через запятую можно перечислить несколько методов для MFA).AllowUsers username- белый список пользователей, которым разрешен SSH. Заменитеusernameна имя non-root юзера сервера, иначе заблокируете себе вход.MaxAuthTries 3- максимум 3 попытки аутентификации на одно соединение (дефолт 6). Ssh-agent предлагает серверу каждый свой ключ как отдельную попытку, поэтому если в агенте больше 3 ключей, вход может оборваться сToo many authentication failuresеще до нужного ключа. ЛечитсяIdentitiesOnly yesв~/.ssh/config(раздел 12) - тогда предлагается только указанный ключ.LoginGraceTime 20- 20 секунд на аутентификацию, потом разрыв (дефолт 120). Ограничивает время, которое незавершенное соединение держит слот сервера: боты не могут оставить открытыми сотни наполовину установленных сессий и упереться в лимит одновременных подключений.ClientAliveInterval 300+ClientAliveCountMax 2- сервер раз в 5 минут проверяет, жив ли клиент; после 2 фейлов разрывает. Убивает зависшие сессии через 10 минут молчания.X11Forwarding no- на сервере нет графики, поэтому отключаем.AllowAgentForwarding no- не даем пробрасывать ssh-agent дальше через сервер.
В дефолтном sshd_config сверху стоит Include /etc/ssh/sshd_config.d/*.conf. Sshd для большинства директив берет первое встретившееся значение (исключения отмечены в man sshd_config), поэтому drop-in, подключенный Include в самом верху, перебивает то, что идет ниже в основном конфиге.
Проверяем синтаксис до применения и перечитываем конфиг, если ок:
sudo /usr/sbin/sshd -t
sudo systemctl reload ssh
На Ubuntu 24.04 SSH работает через socket-activation - порт 22 слушает systemd-юнит ssh.socket, а сам sshd поднимается по запросу. Директивы аутентификации и таймаутов из нашего файла подхватываются обычным reload.
Не закрывая текущую root-сессию, в новом окне проверяем вход под non-root по ключу: ssh username@<IP_VPS>. Только убедившись, что вход работает, закрываем root.
3. Файрвол UFW
UFW (Uncomplicated Firewall) - это обертка над iptables. Закрываем все входящее, оставляем 22, 80, 443:
sudo ufw default deny incoming # дефолт - блокировать входящий трафик
sudo ufw default allow outgoing # дефолт - разрешить исходящий трафик
# открываем порты на ssh
sudo ufw allow 22/tcp
# открываем порты на http(s), если нужно сервисам
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# включаем файрвол
sudo ufw enable
# посмотреть текущее состояние
sudo ufw status verbose
Вывод последней команды будет таким:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
80/tcp (v6) ALLOW IN Anywhere (v6)
443/tcp (v6) ALLOW IN Anywhere (v6)
Если в будущем планируется ставить какие-то сервисы, то они должны работать на 127.0.0.1 и проксироваться через реверс-прокси (например, Caddy) на 80/443. Не открываем новые порты наружу без причины.
4. fail2ban от брутфорса
fail2ban защищает от перебора паролей. Он читает логи, находит неудачные попытки логина и временно банит IP.
sudo apt install -y fail2ban
На Ubuntu пакет fail2ban из коробки включает jail для SSH через файл /etc/fail2ban/jail.d/defaults-debian.conf. То есть после apt install сервис уже защищает SSH с дефолтными параметрами - больше ничего делать не надо. Проверим:
sudo fail2ban-client status sshd
Должно показать Status for the jail: sshd и счетчики Currently failed, Currently banned.
Если хочется свои параметры
Создаем отдельный файл /etc/fail2ban/jail.d/sshd.conf:
[sshd]
maxretry = 3
bantime = 3600
findtime = 600
maxretry - сколько неудачных попыток входа до бана, bantime - длительность бана в секундах (3600 = час), findtime - окно в секундах, внутри которого считаются эти попытки (600 = 10 минут). Комментарии пишутся через ; .
Файлы в jail.d/ читаются в алфавитном порядке, секции с одинаковым именем мержатся, и значение из файла, прочитанного позже, побеждает. sshd.conf идет после defaults-debian.conf, поэтому наши параметры перекрывают дефолтные.
jail.local - тоже официальный способ переопределять настройки, равноценный jail.d/. В этом гайде используем отдельный файл jail.d/sshd.conf, чтобы свои правки лежали обособленно. Если смешивать оба, помни порядок чтения: jail.conf -> jail.d/*.conf -> jail.local -> jail.d/*.local, и jail.local как прочитанный позже перекроет jail.d/sshd.conf.
Применить:
sudo systemctl reload fail2ban
sudo fail2ban-client status sshd5. Автообновления безопасности
unattended-upgrades - механизм автоматических апдейтов Ubuntu/Debian.
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
На вопрос “Automatically download and install stable updates?” отвечаем yes.
--priority=lowнужен, чтобы мы получили диалоговое окно с подтверждением автоапдейтов.
Проверяем, в /etc/apt/apt.conf.d/20auto-upgrades должно быть:
# Раз в день обновляем список пакетов
APT::Periodic::Update-Package-Lists "1";
# Раз в день запускаем установку обновлений
APT::Periodic::Unattended-Upgrade "1";
Дополнительно я настроил автоматическую очистку старых версий ядер. На VPS раздел /boot обычно небольшой, и пакеты со старыми ядрами могут его забить, из-за чего следующее обновление ядра упадет.
sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-localUnattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Перезагрузку после обновлений ядра оставляем ручной: автоматический ребут на проде нежелателен, момент лучше выбрать самому. Раз в месяц можно делать sudo reboot.
6. Базовые утилиты
sudo apt install -y curl wget htop ncdu tmux tree
htop - мониторинг процессов, ncdu - найти кто ест диск, tmux - детачаемые сессии, tree - визуализация директорий.
7. Часовой пояс и hostname
Часовой пояс необходимо настроить, чтобы логи писались и cron-таски работали по твоему времени, а не по UTC.
Hostname задаем, чтобы в приглашении шелла и логах видеть осмысленное имя машины, а не случайное, и различать хосты при нескольких серверах.
sudo timedatectl set-timezone <таймзона>
sudo hostnamectl set-hostname <хостнейм>8. Выделение swap
При нехватке памяти Linux убивает процесс через OOM-killer - механизм ядра, который при исчерпании RAM выбирает и завершает процесс. Swap не отменяет OOM, но дает буфер - при кратковременной нехватке памяти страницы вытесняются на диск, и процесс не падает сразу. Размер берут от 2x RAM. Для 2 GB я взял 4G с запасом.
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h9. Git, Docker + Docker Compose
sudo apt install -y git
Дока Docker Engine: https://docs.docker.com/engine/install/ubuntu/
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
После добавления в группу docker нужно перелогиниться, чтобы изменения применились. Проверка:
docker run hello-world10. DigitalOcean Metrics Agent
Разделы 10-11 про базовую настройку метрик средствами DO. Помимо этого у меня работает Prometheus + Grafana / Alerts, об этом будет в отдельном гайде.
Дока DO Metrics Agent: https://docs.digitalocean.com/products/monitoring/how-to/install-metrics-agent/
Без do-agent DO видит только базовые внешние метрики. С ним работают расширенные графики, алерты по памяти/диску/CPU.
curl -sSL https://repos.insights.digitalocean.com/install.sh -o /tmp/install.sh
less /tmp/install.sh # просмотреть, что делает
sudo bash /tmp/install.sh # запустить
systemctl status do-agent # проверить
Агент работает под отдельным пользователем do-agent.
11. Resource Alerts в DO
После установки do-agent создаем в Monitoring -> Create Alert Policy:
- Disk Utilization > 85% / 5 min
- Memory Utilization > 90% / 5 min
- CPU Utilization > 95% / 10 min
- Public Outbound Bandwidth > 100 Mbps / 10 min - сигнал о взломе.
- Account -> Billing Alert > $20 - страховка от неожиданных расходов
Пороги со временем (например, через неделю наблюдений) стоит скорректировать под реальную нагрузку.
12. SSH-конфиг на локальной машине
На локальной машине добавить в ~/.ssh/config:
Host vps
HostName <IP_VPS>
User username
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3
vps - произвольный алиас для подключения. ServerAliveInterval шлет keepalive, чтобы соединение не отваливалось при долгом простое. IdentitiesOnly yes заставляет ssh предлагать только указанный IdentityFile, а не все ключи из агента - это важно в паре с MaxAuthTries 3 из раздела 2, иначе при многих ключах в агенте можно упереться в Too many authentication failures.
Теперь вместо ssh username@<IP_VPS> достаточно ssh vps.
13. Бэкапы DO
Бэкап средствами DigitalOcean: Droplets -> дроплет -> Backups & Snapshots -> Setup Automated Backups -> Weekly Backups.
Стоит 20% стоимости дроплета.
14. Лимит логов journald
Ограничим journald место под логи. Основной /etc/systemd/journald.conf напрямую не трогаем (по той же причине, что и sshd_config в разделе 2):
sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/00-server.conf[Journal]
SystemMaxUse=500M
Применить и подрезать уже накопившиеся логи разово:
sudo systemctl restart systemd-journald
sudo journalctl --vacuum-size=500M
Полезные приемы journalctl:
journalctl -f # live tail всего журнала
journalctl -f -u ssh # live tail сервиса
journalctl --since "1 hour ago" # за последний час
journalctl --since "2026-05-17 14:00" # с конкретного времени
journalctl -k # только сообщения ядра
journalctl -p err -b -u ssh # только errors+, текущая загрузка, конкретный юнит