Чёрный серверный блейд в тёмном дата-центре, окружённый светящимся изумрудным шестиугольным щитом; летящие зелёные частицы света разбиваются о барьер
Руководство по безопасности

Защита нового VPS в первый час

Публичный IPv4-адрес начинают прощупывать через считаные минуты после первого же пакета — машины, которые никогда о вас не слышали и никогда не услышат. И это хорошая новость: почти всё, что нацелено на новый сервер, — типовое, и один осознанно потраченный час отражает почти всё это. Тот же час, потраченный в неправильном порядке, запирает вас снаружи машины, войти в которую вместо вас не сможет никто.

Мы выдаём вам root-шелл примерно через 47 секунд после подтверждения оплаты — и затем сознательно останавливаемся. Мы не устанавливаем агент, не управляем вашим файрволом и не храним копию ваших учётных данных — ключ, который храним мы, это ключ, который у нас могут заставить выдать, и этот принцип пронизывает всю платформу. Следствие простое, и его стоит сказать прямо: безопасность вашего сервера — это то состояние, в котором вы оставите его в первый час.

Дальше описан этот час — в том порядке, в котором мы сами его проходим, на Debian 13 и Ubuntu 24.04. Основную часть работы делают две настройки. Остальная часть страницы существует из-за трёх вещей, которые выглядят правильно, проходят финальную проверку любого туториала и при этом незаметно не работают: SSH-конфиг в виде drop-in файла, который тихо переопределяется; настройка порта, которую socket-activated sshd игнорирует; и контейнерный runtime, публикующий порты в обход вашего файрвола. На каждой из них уже кто-то обжёгся, сделав всё остальное правильно.

Кто на самом деле стучится в дверь

Посмотрите на journalctl -u ssh на сервере, который был онлайн всего час, — объём записей пугает при первом взгляде. Пугаться не стоит. Перед вами фоновое излучение интернета: горстка сканирующих операций — часть научных, часть коммерческих, часть криминальных — которые непрерывно перебирают всё пространство IPv4 и передают результаты ботам, подбирающим учётные данные. До вашего адреса добрались потому, что он существует, по порядку номеров, и до него доберутся снова через несколько часов, что бы вы ни делали.

Это полезным образом меняет форму задачи. Вы защищаетесь не от противника, который выбрал именно вас и будет подстраиваться, а от скрипта с фиксированным набором приёмов: root, admin, ubuntu, test, git, oracle, postgres и двадцать тысяч паролей, засветившихся в утечках. У него нет ни терпения, ни изобретательности, ни интереса к машине, которая не ответила с первой попытки. Отключение парольной аутентификации не замедляет этого противника — оно убирает его с доски полностью.

Стоит знать две детали. Во-первых, IPv6 значительно тише, потому что /64 нельзя просканировать целиком, — но в тот момент, когда ваша AAAA-запись становится публичной или адрес машины попадает в заголовок письма или в лог certificate-transparency, тишина заканчивается. Никогда не считайте IPv6 укрытием — считайте его просто меньшим стогом сена. Во-вторых, у IPv4-адреса есть прошлое. До вас у него был другой арендатор, и если тот плохо администрировал почтовый сервер или хостил что-то, что попало в чёрные списки, вы наследуете эту репутацию, пока она не сойдёт на нет. Если почта для вас важна, проверьте адрес по обычным блок-листам, прежде чем строить на нём что-либо, — это пятиминутная проверка, которая экономит две недели разбирательств с доставляемостью писем.

Две настройки, которые делают девяносто процентов работы

Почти любой реальный взлом небольшого сервера начинается в одном из двух мест: пароль, который можно было подобрать, или служба, которая слушала порт, хотя не должна была. Соответствующие меры — это PasswordAuthentication no и файрвол с политикой по умолчанию drop. Ничего эффектного, вместе они занимают пятнадцать минут — и стоят больше, чем все остальные меры на этой странице, вместе взятые.

Причина здесь структурная, а не статистическая. Обе меры закрыты по умолчанию — они безопасно отказывают. sshd, принимающий только ключи, невозможно взломать перебором, сколько бы попыток ни пришло, потому что в коде просто нет пути, принимающего пароль. Файрвол с политикой default-deny защищает и те службы, которые вы ещё не установили, — включая базу данных, которую вы добавите через три месяца и забудете привязать к localhost. Всё остальное в чек-листе по харденингу — это перечисление конкретных плохих вещей, список того, что нужно отключить, а такой список хорош ровно настолько, насколько он полон.

Так что если вы читаете только один раздел и закрываете вкладку — читайте этот, выполните шаги со 2 по 5 ниже и считайте час потраченным не зря. Всё остальное по-настоящему полезно, но по-настоящему второстепенно.

Где теперь живёт конфигурация SSH и в чём тут ловушка

На Debian 13 и Ubuntu 24.04 файл /etc/ssh/sshd_config начинается со строки Include /etc/ssh/sshd_config.d/*.conf, а облачные образы кладут в этот каталог файл — обычно 50-cloud-init.conf — который уже задаёт PasswordAuthentication. Редактировать основной файл и дописывать свои директивы в конец кажется естественным, но в результате получается конфиг, который делает не то, что в нём написано.

Правило, которое удивляет в sshd почти всех: для каждого ключевого слова побеждает первое полученное значение. Это противоположность тому, как работает почти любая другая система слияния конфигов, с которой вы имели дело. Строка Include находится в самом начале основного файла, поэтому drop-in читается раньше тела sshd_config — а среди самих drop-in-файлов решает алфавитный порядок. Файл 10-hardening.conf побеждает 50-cloud-init.conf. Файл 60-hardening.conf, наоборот, тихо проигрывает ему, и вы не узнаете об этом из перезапуска, который бодро отрапортует об успехе.

Именно поэтому никогда не верьте файлу, который только что написали. Спросите у самого демона, что он на самом деле прочитал:

sshd -t                 # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) '

sshd -T выводит итоговую конфигурацию уже после того, как учтены все include, все переопределения и все значения по умолчанию. Если в этом выводе значится passwordauthentication yes, значит, парольная аутентификация включена — независимо от того, что утверждает любой файл на диске. Ничто другое не считается подтверждением.

Вторая ловушка из той же серии. Ubuntu 24.04 запускает sshd через активацию по сокету: слушающий порт принадлежит ssh.socket, а директива Port в sshd_config полностью игнорируется. Проверьте это командой systemctl is-enabled ssh.socket; если сокет включён и вам нужен другой порт, меняйте его через systemctl edit ssh.socket и переопределение ListenStream= — а не в sshd_config, где настройка будет выглядеть правильной и не делать ничего.

Перенос с порта 22: театр, но дешёвый театр

Будем честны в этом вопросе, потому что интернет — нет. Перенос sshd на порт 2222 или 47000 не останавливает ни одного компетентного атакующего. Полное TCP-сканирование одного хоста занимает секунды, служба сама называет себя в баннере, и всякий, кто решил атаковать именно вас, найдёт её раньше, чем допьёт кофе.

Что это действительно даёт — так это сокращение лога аутентификации примерно на девяносто пять процентов, и в этом есть реальная польза: это разница между логом, который вы бегло просматриваете, и логом, который вы действительно читаете. Сигнал, который видно, лучше сигнала, который погребён под шумом. Если вы вообще что-то мониторите, тихий auth.log — самый дешёвый способ сделать аномалию заметной.

Издержки невелики, но они реальны, и именно поэтому мы называем это опциональным, а не рекомендуемым. Вы забудете порт, когда вернётесь к серверу через восемь месяцев. Нестандартный порт выше 1024 в принципе может быть перехвачен непривилегированным процессом, если sshd вдруг перестанет его слушать. Строгие исходящие файрволы в клиентских сетях блокируют нестандартные порты, так что временами вы не сможете достучаться до собственного сервера из офиса или отеля. А в системе с активацией по сокету менять порт нужно в правильном месте — как описано в предыдущем разделе. Делайте это, если тихие логи для вас важны. Не делайте это и не чувствуйте себя после этого в безопасности, и никогда не делайте это вместо входа только по ключам.

Default-deny и набор правил, который мы реально используем

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

На нашей платформе у вас два независимых друг от друга уровня. Фильтр на границе — опциональный, настраивается для каждого сервера в панели управления и применяется на уровне гипервизора, поэтому заблокированный трафик вообще не доходит до вашей гостевой системы, — он полезен для правил уровня 4, которые нужно применить прежде, чем ваше ядро потратит на пакет хоть один такт, а также чтобы скомпрометированная гостевая система оставалась недоступной, пока вы с ней разбираетесь. Файрвол внутри гостевой системы — целиком ваш: nftables, iptables, pf — что бы ни поставлялось с вашим образом. Мы никогда его не трогаем. Используйте оба уровня, если машина важна; используйте как минимум второй — всегда.

Два момента в наборе правил из шага 5 заслуживают объяснения, потому что в большинстве скопированных откуда попало наборов правил их делают неправильно. Не дропайте весь ICMP целиком. Это кажется аккуратным решением, но оно ломает обнаружение path-MTU, а это порождает худший из возможных классов багов: маленькие запросы работают, большие ответы зависают, и ни в одном логе нет ни слова про файрвол. Разрешите как минимум destination-unreachable, time-exceeded и parameter-problem. Не фильтруйте ICMPv6 по типу, если не знаете весь список типов. IPv6 полагается на ICMPv6 для обнаружения соседей и анонсов маршрутизаторов; заблокируйте его широким мазком — и связность по IPv6 умрёт так, что это будет выглядеть как проблема с маршрутизацией. Разрешить весь ICMPv6 целиком на одиночном хосте — разумный компромисс, и набор правил ниже именно так и делает.

Одно предупреждение перед тем, как вы выполните flush ruleset: если установлен Docker, эта строка удаляет NAT- и filter-правила, записанные Docker, и сетевая связность контейнеров пропадает, пока systemctl restart docker не вернёт их обратно. Сначала загрузите свой набор правил, затем перезапустите Docker — и прочитайте следующий раздел, прежде чем публиковать хотя бы один порт контейнера.

Порты, о которых вы не знали, что они открыты

Спросите у машины, что она слушает. Не то, что вы, как вам кажется, установили, — а то, что реально забиндено прямо сейчас:

ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'

Всё, что проходит через этот фильтр, доступно откуда-то ещё, кроме самой машины. На стандартном образе обычно находятся rpcbind на 111 (он не нужен ничему из того, что вы запускаете), слушатель exim4, оставшийся от базовой установки, и заглушка systemd-resolved, безобидная на loopback и превращающаяся в открытый резолвер, если это не так. Опасные находки появляются позже, вместе с софтом, который вы поставили сознательно: PostgreSQL на 0.0.0.0:5432, потому что какой-то туториал велел отредактировать listen_addresses; Redis без пароля, потому что у Redis почти всю его историю не было пароля по умолчанию; узел Elasticsearch; экспортер Prometheus; Jupyter notebook. Каждая из этих вещей не раз становилась первым шагом настоящего взлома.

А теперь ловушка, в которую попадают даже аккуратные люди. Docker не спрашивает разрешения у вашего файрвола. Когда вы пишете -p 5432:5432, Docker добавляет DNAT-правила в цепочку PREROUTING таблицы nat, которую ядро обрабатывает раньше, чем пакет вообще попадёт в вашу цепочку input; трафик после этого перенаправляется в контейнер. Ваша политика default-deny для input вообще не спрашивается, ufw status показывает порт закрытым, а база данных при этом торчит в публичном интернете. Это задокументированное поведение Docker, оно такое уже десять лет, и на нём засветилось огромное количество баз данных.

Исправление умещается в одну строку, и стоит выработать привычку её использовать:

# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres

# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres

И напоследок — получите взгляд со стороны. Скан портов с самого сервера говорит о том, что думает ядро; скан откуда-то ещё говорит о том, что видит мир, а это единственная цифра, которая имеет значение. Запустите nmap -Pn -p- <your-ip> с вашего ноутбука, а не с самой машины, и сравните результат со списком, который вы ожидали увидеть.

Автоматические обновления: включите их по-настоящему

Небольшие серверы взламывают не через уязвимости нулевого дня. Их взламывают в те четыре недели между публикацией CVE вместе с рабочим proof-of-concept и следующим разом, когда вы случайно зайдёте на сервер. Инструменты для атак осваивают новый удалённый баг за считаные дни; машина, которую вы патчите «как руки дойдут», остаётся уязвимой весь этот промежуток, и любой честный администратор знает, насколько на самом деле длинным бывает этот промежуток.

Возражение против автообновлений — страх что-то сломать, и его стоит воспринимать всерьёз, а затем сузить его рамки. Ограничьте автоматизацию security-веткой вашего дистрибутива, где мейнтейнеры переносят исправления в уже собранную версию пакета, а не выпускают новые версии апстрима. Обновление безопасности Debian для openssl — это пропатченная сборка той же версии, что у вас уже стоит; риск, что оно изменит поведение, намного меньше риска оставить удалённо эксплуатируемую дыру открытой на месяц. Обновления с новыми фичами по-прежнему остаются ручными — там им и место.

Часть, которую все пропускают, — это перезагрузка. Пропатченная на диске libssl ничего не даёт процессу, который замапил старую версию библиотеки при запуске, а обновление ядра вообще ничего не даёт, пока вы в него не загрузитесь. Либо согласитесь на автоматическую перезагрузку в выбранное вами окно, либо поставьте needrestart и пусть он сообщает, какие службы работают с уже удалёнными библиотеками, — но сделайте хоть что-то одно. «Автообновления включены» при uptime в 340 дней — это удобная иллюзия, и это самая частая иллюзия, которую мы видим.

fail2ban, CrowdSec или вообще ничего

fail2ban читает ваши логи, замечает повторяющиеся неудачные попытки с одного адреса и на время банит его. CrowdSec делает то же самое и вдобавок делится вердиктами по сети участников, так что вы можете заблокировать адрес, который уже успел где-то напакостить, ещё до того, как он доберётся до вас. Обе программы хороши. Но ни одна из них не делает того, что думает большинство людей, если речь о SSH-сервере, принимающем только ключи.

Как только PasswordAuthentication выставлен в no, перебор пароля по SSH не может увенчаться успехом. Не «маловероятен» — а именно не может, потому что для этого просто нет пути в коде. Поэтому бан адреса после пяти неудачных попыток ничего не предотвращает — он лишь сокращает объём логов и ничтожную долю нагрузки на CPU. Это реальная польза, просто не польза для безопасности, и плата за неё не нулевая: неудачно написанный jail, который смотрит не в тот лог, запирал администраторов вне их собственных серверов чаще, чем когда-либо запирал снаружи атакующих.

Там, где эти инструменты по-настоящему оправдывают себя, — это на уровень выше, на службах, которые вы действительно выставляете наружу: форма входа, админка WordPress, API с рейт-лимитами, почтовый сервер с SMTP AUTH. Вот они действительно принимают пароли, их действительно можно перебирать, и список банов — именно то средство, которое здесь нужно. Поэтому наш вердикт узкий и конкретный. Пропустите jail для SSH. Поставьте CrowdSec или fail2ban перед тем, что реально имеет поле для пароля. И что бы вы ни выбрали, сначала занесите в белый список собственный адрес для управления — ignoreip существует именно для того вечера, который иначе запомнится вам совсем не с той стороны.

Как заставить машину сообщать вам об изменениях

Предотвращение — это то, что можно сделать за час. Обнаружение — это то, что скажет вам, хватило ли этого часа. Оно не обязано быть сложным: на одиночном сервере три дешёвых меры закрывают большую часть поля.

Сохраняйте логи. На многих образах журнал живёт в /run и испаряется при перезагрузке, а значит, запись об инциденте исчезает при следующей же перезагрузке. Создание /var/log/journal — правка в одну строку и самая ценная вещь в этом разделе.

Узнавайте о входах в систему. Строка в /etc/ssh/sshrc, вызывающая logger при каждом начале сессии, ничего не стоит и даёт вам чистую, удобную для grep запись отдельно от собственной болтовни sshd. Есть нюанс, на котором люди спотыкаются: sshd запускает ~/.ssh/rc вместо /etc/ssh/sshrc, если у пользователя есть свой файл, — то есть общесистемный файл пропускается именно для той учётной записи, у которой скорее всего есть свой кастомный dotfile. Если вам нужен хук, который нельзя перекрыть, используйте вместо этого pam_exec.

Знайте, как выглядела файловая система в первый день. AIDE записывает хеши ваших бинарников, библиотек и юнит-файлов и сообщает, что изменилось с тех пор. Загвоздка здесь фундаментальная, и её обычно игнорируют: базу, хранящуюся на той же машине, за которой она следит, может пересоздать тот, кто эту машину скомпрометировал, — и после этого проверка будет вечно докладывать «изменений нет». Скопируйте базу за пределы сервера или хотя бы запишите её хеш где-то ещё — тогда инструмент будет стоить потраченных на него двадцати минут. Оставленный как есть, он — не более чем плюшевый мишка для успокоения.

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

Чего это не делает

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

Это не защищает данные на работающей машине. Укреплённое ядро и закрытый файрвол не имеют никакого значения для того, у кого есть доступ к гипервизору, и для содержимого оперативной памяти, пока машина включена. Если вас беспокоит диск в момент, когда сервер выключен, изъят или списан, — это шифрование диска, и это другая процедура с другим сценарием отказа — см. шифрование диска VPS с помощью LUKS.

Это не делает вас анонимным. Сервер может быть безупречно защищён и при этом выдавать, кому он принадлежит, — через запись WHOIS, повторно используемый SSH-ключ, аналитический тег, SSH-клиент, который подключается прямо из дома без промежуточного узла, или сертификат, связывающий две личности между собой. Оплата через Monero с последующим входом с вашего собственного адреса сводит эту оплату на нет. У этого сценария отказа есть отдельная страница: ошибки, которые вас деанонимизируют.

Это не чинит ваше приложение. SQL-инъекции, неаутентифицированной админ-ручке или зависимости с бэкдором совершенно всё равно, какие у вас настройки sshd. Большинство взломов хорошо настроенных серверов происходит через порт, который вы сами, сознательно, открыли для написанной или установленной вами службы.

Это не резервная копия. Ransomware, rm -rf с переменной, которая оказалась пустой, и неудачное обновление — всё это заканчивается одинаково. Сделайте копию, положите её в другое место и хотя бы раз восстановитесь из неё до того, как это понадобится по-настоящему, — та же дисциплина, что делает переживаемым переезд на другой хостинг, делает переживаемым и один неудачный вторник.

Ничто из этого не довод против того самого часа. Это довод в пользу того, чтобы точно знать, что именно этот час вам купил.

  1. Разворачивайте сервер с ключом, а не с паролем

    Сгенерируйте пару ключей на своей машине и вставьте публичную половину в форму развёртывания; образ установит её сам, и у вас никогда не будет root-пароля, который можно потерять. Если вы разворачиваете сервер из образа с пометкой cloud-init, вместо этого можно передать всю конфигурацию первого часа как user-data — до 64 КиБ, — и машина поднимется уже защищённой.

    # on your laptop, not on the server
    ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh"
    cat ~/.ssh/id_ed25519.pub        # paste this into the deploy form

    Используйте ed25519, если только что-то в вашем инструментарии от него не отказывается, — в этом случае вполне подойдёт RSA на 4096 бит. Защитите приватный ключ парольной фразой и загрузите его в агент; файл ключа без парольной фразы на ноутбуке — это как пароль, записанный на мониторе. Тот же набор ключей внедряется и в режим восстановления, и именно это делает шаг 4 обратимым.

  2. Откройте вторую сессию, прежде чем что-либо трогать

    Это не опция и не паранойя. Дальше вы будете менять демон, через который подключены, и файрвол, который вообще даёт до него достучаться. Держите одну сессию открытой и простаивающей — как страховочный трос обратно в машину, — а все изменения вносите в другой сессии. Если что-то пойдёт не так, открытая сессия всё ещё аутентифицирована и может откатить изменение; новое же подключение будет отклонено.

    # terminal A — the rope. Log in and leave it alone.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>
    
    # terminal B — where every command below runs.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>

    Проверяйте каждое изменение, открывая третье соединение, а не переподключаясь в той сессии, где работаете. Если третье соединение не удастся, у вас всё ещё есть два рабочих шелла и обычная проблема — а не катастрофа.

  3. Создайте учётную запись, которой будете реально пользоваться

    Root по SSH удобен ровно один час. После этого именная учётная запись с sudo даёт вам аудиторский след, защищает от опечатки в команде, выполненной с полными правами, и позволяет отключить скомпрометированную учётку, не отключая саму машину.

    adduser --disabled-password --gecos "" deploy
    install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
    chown deploy:deploy /home/deploy/.ssh/authorized_keys
    chmod 0600 /home/deploy/.ssh/authorized_keys
    usermod -aG sudo deploy
    
    # a long random password, stored in your manager, used only by sudo
    passwd deploy

    Обязательно задайте этот пароль. Учётка с sudo, для которой пароль так и не был задан, не может пользоваться sudo, а обнаружить это уже после отключения входа под root — классический способ запереть себя снаружи при том, что все файлы настроены правильно. Проверьте, прежде чем двигаться дальше: из нового терминала выполните ssh deploy@<server-ip>, затем sudo -v. Обе команды должны сработать.

  4. Напишите SSH drop-in, затем спросите у демона, что он прочитал

    Пронумерованный drop-in, который сортируется раньше остальных, побеждает всё, что уложил туда облачный образ, — согласно правилу порядка выше. Напишите его, проверьте синтаксис, прочитайте итоговую конфигурацию — и только потом делайте reload.

    cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    AllowUsers deploy
    MaxAuthTries 3
    LoginGraceTime 20
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no      # remove this line if you use ssh -L / -D tunnels
    ClientAliveInterval 300
    ClientAliveCountMax 2
    EOF
    chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf
    
    sshd -t                                    # silence means the syntax is valid
    sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) '
    systemctl reload ssh

    Прочитайте вывод sshd -T до reload, а не после. В нём должно быть permitrootlogin no и passwordauthentication no. Если это не так, значит ваш файл переопределяется тем, что сортируется раньше него, — выполните ls /etc/ssh/sshd_config.d/ и переименуйте свой файл так, чтобы он шёл позже. Теперь откройте третий терминал и зайдите под deploy. Только когда это сработает, можно закрывать страховочную сессию.

  5. Загрузите файрвол с политикой default-deny

    nftables поставляется в обоих дистрибутивах и заменяет набор правил iptables одним читаемым файлом. Подстройте разрешённые порты под службы, которые вы реально запускаете, — список ниже предполагает только SSH и веб-сервер, и ничего больше.

    apt install -y nftables
    cat > /etc/nftables.conf <<'EOF'
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority filter; policy drop;
    
        ct state established,related accept
        ct state invalid drop
        iif lo accept
    
        # ICMP must live: dropping it blackholes path-MTU discovery.
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept
        ip6 nexthdr icmpv6 accept
    
        tcp dport 22 ct state new accept
        tcp dport { 80, 443 } ct state new accept
    
        limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info
      }
      chain forward { type filter hook forward priority filter; policy drop; }
      chain output  { type filter hook output  priority filter; policy accept; }
    }
    EOF
    
    nft -c -f /etc/nftables.conf              # check before you commit to it
    systemctl enable --now nftables
    nft list ruleset | head -40

    Если у вас установлен Docker, flush ruleset заодно удаляет и его правила: после этого выполните systemctl restart docker и убедитесь, что контейнеры снова отвечают. Затем, с ноутбука, выполните nmap -Pn -p- <server-ip> и проверьте, что список открытых портов совпадает с тем, что вы только что разрешили, — не больше и не меньше.

  6. Включите автоматические обновления безопасности

    Только security-ветка, и с окном перезагрузки, которое вы выбрали сами, а не откладываете бесконечно. Выберите час, спокойный для ваших пользователей, и не ровно на границе часа, чтобы не столкнуться с чужими cron-задачами.

    apt install -y unattended-upgrades needrestart
    cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF'
    // Debian. On Ubuntu, replace the two Origins-Pattern lines with:
    //   "${distro_id}:${distro_codename}-security";
    Unattended-Upgrade::Origins-Pattern {
      "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
      "origin=Debian,codename=${distro_codename},label=Debian-Security";
    };
    Unattended-Upgrade::Automatic-Reboot "true";
    Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
    Unattended-Upgrade::Automatic-Reboot-Time "04:17";
    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
    APT::Periodic::Update-Package-Lists "1";
    APT::Periodic::Unattended-Upgrade "1";
    EOF
    
    unattended-upgrade --dry-run --debug | tail -25
    systemctl status unattended-upgrades --no-pager

    Если автоматическая перезагрузка действительно неприемлема, установите здесь "false" и позвольте needrestart -b сообщать, какие службы работают со старыми удалёнными библиотеками и не появилось ли более новое ядро, — а затем действуйте по результатам. Неприемлемо только одно: не делать ни того, ни другого.

  7. Закройте лишние слушающие порты и проверьте снаружи

    Перечислите всё, что привязано не только к loopback, уберите то, чем не пользуетесь, и подтвердите результат с машины, которая не является этим сервером.

    ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'
    
    # the usual leftovers on a base image
    systemctl disable --now rpcbind.socket rpcbind 2>/dev/null
    apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null
    apt autoremove --purge -y
    
    # what each running unit is allowed to reach
    systemd-analyze security --no-pager | head -20

    Для всего, что должно продолжать работать, но не нуждается в зрителях, — база данных, кеш, админ-панель, экспортер метрик, — привяжите это к 127.0.0.1 в собственной конфигурации и обращайтесь через SSH-туннель: ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. Это строго лучше, чем открывать порт и надеяться, что встроенная аутентификация службы достаточно надёжна, — и это шаблон, к которому стоит прибегать по умолчанию.

  8. Зафиксируйте базовое состояние и настройте сигнализацию

    Десять минут, которые окупаются лишь в тот день, когда что-то пойдёт не так, — а это именно тот день, когда восстановить всё по памяти уже не получится.

    # persistent journal, so a reboot stops erasing the evidence
    mkdir -p /var/log/journal
    systemd-tmpfiles --create --prefix /var/log/journal
    systemctl restart systemd-journald
    
    # a clean, greppable line per SSH session
    cat > /etc/ssh/sshrc <<'EOF'
    logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}"
    EOF
    
    # file-integrity baseline — then get the database off the machine
    apt install -y aide
    aideinit
    mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
    sha256sum /var/lib/aide/aide.db     # copy this hash somewhere else
    
    last -n 20 ; lastb -n 20            # who got in, and who tried

    Завершите тем, что запишете за пределами сервера четыре вещи: отпечаток хост-ключа SSH из ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, хеш AIDE из шага выше, порты, которые вы сознательно открыли, и место, где хранится приватный ключ. Именно эта заметка превращает плохое утро в чек-лист, а не в расследование.

Сравнение

Меры защиты, ранжированные по тому, что они реально дают

Каждая мера первого часа проверена по двум вопросам, которые решают, стоит ли она места на странице: что она действительно останавливает и чего не останавливает — что бы ни утверждали туториалы.
МераЧто она останавливаетЧего она не останавливаетСтоимостьВердикт
SSH только по ключам (PasswordAuthentication no)Любого бота, подбирающего пароли, — навсегда и по самой конструкцииЛюбого, кто владеет вашим приватным ключом, или уязвимость в самом sshd5 минут, один разОбязательно
Файрвол default-deny (nftables)Службы, о которых вы забыли, что они слушают, и любую из тех, что вы случайно установите потомАтаки на порты, которые вы сознательно открыли10 минут, один разОбязательно
Автоматические обновления безопасностиОкно n-day — недели между публичным эксплойтом и вашим следующим входомУязвимости нулевого дня и всё, что требует перезагрузки, которую вы так и не запланировали5 минут плюс окно перезагрузкиОбязательно
Именная учётка с sudo вместо rootОпечатки в командах с полными правами и процессы, работающие от root без необходимостиЛокальное повышение привилегий в руках того, кто уже внутри3 минутыОправданно
Базовый снимок целостности файлов (AIDE)Незаметные правки системных бинарников, библиотек и юнит-файловВообще ничего, если база остаётся на той же машине, за которой следит20 минут плюс хранение вне сервераОправданно, если сервер важен
Перенос SSH с порта 22Примерно 95% объёма лога аутентификацииЛюбого, кто запустит скан портов, — то есть всех, кто действительно важен2 минуты плюс вечер, когда вы забудете портОпционально, ради тихих логов
fail2ban / CrowdSec на sshdРецидивистов и трату CPU, которую они на вас навешиваютНичего — как только парольная аутентификация отключена10 минут плюс реальный риск заблокировать самого себяПропустить для SSH; использовать для приложения
FAQ

Ответы на актуальные вопросы

Через сколько времени после развёртывания начинается сканирование?

Обычно — через считаные минуты. Сканеры, охватывающие весь интернет, непрерывно проходят всё пространство IPv4, так что свежий адрес будет достигнут при следующем же проходе — независимо от того, что на нём работает. Считайте, что вас начинают прощупывать с момента, когда машина ответила на первый же пакет, — именно поэтому защиту настраивают в первый час, а не в первые выходные.

Нужен ли fail2ban, если вход по паролю отключён?

Для SSH — нет. При PasswordAuthentication no у перебора пароля просто нет пути к успеху, поэтому бан после пяти неудачных попыток ничего не предотвращает — он лишь снижает шум в логах, а это кое-чего стоит, но не является безопасностью. Место этим инструментам — перед всем, что реально принимает пароль: веб-форма входа, админ-панель, SMTP AUTH. Если вы всё же ставите такой инструмент, сначала занесите в белый список собственный адрес.

ufw, firewalld или nftables?

Любой из них — при условии, что политика по умолчанию drop и вы понимаете, что именно развернули. ufw самый дружелюбный и на современных Debian и Ubuntu под капотом пишет правила nftables. Обычный nftables — это один читаемый файл, который можно диффать и хранить в системе контроля версий, поэтому мы используем именно его. Выбор конкретного инструмента значит намного меньше, чем политика по умолчанию, — и ни один из трёх вариантов не отменяет того, что Docker публикует порты в обход всех них.

Стоит ли переносить SSH с порта 22?

Только ради более тихих логов. Это не останавливает ни одного компетентного атакующего — полное сканирование занимает секунды, а баннер сам называет службу. Зато это убирает большую часть шума из лога аутентификации, благодаря чему реальные аномалии становятся заметны. Относитесь к этому как к гигиене логов, а не как к защите, и прежде чем менять порт, проверьте, не активируется ли ваш sshd через сокет: на Ubuntu 24.04 директива Port в sshd_config игнорируется, а порт принадлежит ssh.socket.

Не сломают ли автообновления мой сайт в четыре утра?

Если ограничиться веткой security — крайне редко. Такие пакеты содержат перенесённые назад исправления для той же версии, что у вас уже стоит, а не новые релизы апстрима. Реальный риск несёт перезагрузка, а не сам патч, — поэтому выбирайте окно самостоятельно, установите Automatic-Reboot-WithUsers в false, чтобы система ждала, пока кто-то залогинен, а если служба действительно не может перезапускаться без присмотра, запускайте needrestart -b по расписанию и действуйте по его отчётам. Единственная неоправданная позиция — это включённые автообновления при аптайме, который измеряется годами.

Я заблокировал сам себя. Что делать?

Загрузите сервер в режим восстановления из панели управления. Он поднимает окружение Alpine в оперативной памяти с теми же SSH-ключами, что и у вашей учётной записи, а ваш диск при этом отмонтирован и доступен как /dev/vda, — так что вы можете смонтировать его, исправить SSH drop-in или файл файрвола, отмонтировать и перезагрузиться. Сама загрузка в режиме восстановления ничего на диске не трогает. Отпечаток хост-ключа режима восстановления отличается от обычного — это ожидаемо, а не перехват.

Стоит ли вместо этого запустить Lynis или скрипт хардинга по CIS?

Как аудит — да, как замена — нет. Lynis — по-настоящему полезное второе мнение, он найдёт вещи, о которых на этой странице не сказано. Автоматические скрипты CIS-ремедиации — это совсем другая история: они применяют сотни изменений, рассчитанных на парк корпоративных рабочих станций, и несколько из них ломают сервер способом, который потом неделями трудно отследить. Сначала пройдите этот час руками, чтобы понимать, что именно делает ваша машина, а затем запустите аудитор и разбирайте его находки по одной.

Делает ли харденинг мой сервер анонимным?

Нет, и путать эти две вещи — распространённая и дорогая ошибка. Харденинг контролирует, кто может попасть внутрь; анонимность контролирует, кто может узнать, что сервер ваш. Идеально запертый сервер всё равно выдаёт владельца через запись WHOIS, повторно используемый SSH-ключ, аналитический тег или вход в админку с домашнего адреса. Оплата через Monero с последующим прямым подключением с вашего обычного соединения полностью сводит эту оплату на нет — это разобрано в материале об ошибках, которые вас деанонимизируют.

Deploy your offshore server.

Выберите регион. Выберите план. Вставьте ключ. Оплатите. Следующие 47 секунд — за нами.