
Защита нового 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 с переменной, которая оказалась пустой, и неудачное обновление — всё это заканчивается одинаково. Сделайте копию, положите её в другое место и хотя бы раз восстановитесь из неё до того, как это понадобится по-настоящему, — та же дисциплина, что делает переживаемым переезд на другой хостинг, делает переживаемым и один неудачный вторник.
Ничто из этого не довод против того самого часа. Это довод в пользу того, чтобы точно знать, что именно этот час вам купил.
- Разворачивайте сервер с ключом, а не с паролем
Сгенерируйте пару ключей на своей машине и вставьте публичную половину в форму развёртывания; образ установит её сам, и у вас никогда не будет 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 обратимым.
- Откройте вторую сессию, прежде чем что-либо трогать
Это не опция и не паранойя. Дальше вы будете менять демон, через который подключены, и файрвол, который вообще даёт до него достучаться. Держите одну сессию открытой и простаивающей — как страховочный трос обратно в машину, — а все изменения вносите в другой сессии. Если что-то пойдёт не так, открытая сессия всё ещё аутентифицирована и может откатить изменение; новое же подключение будет отклонено.
# 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>Проверяйте каждое изменение, открывая третье соединение, а не переподключаясь в той сессии, где работаете. Если третье соединение не удастся, у вас всё ещё есть два рабочих шелла и обычная проблема — а не катастрофа.
- Создайте учётную запись, которой будете реально пользоваться
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. Обе команды должны сработать. - Напишите 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. Только когда это сработает, можно закрывать страховочную сессию. - Загрузите файрвол с политикой 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>и проверьте, что список открытых портов совпадает с тем, что вы только что разрешили, — не больше и не меньше. - Включите автоматические обновления безопасности
Только 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сообщать, какие службы работают со старыми удалёнными библиотеками и не появилось ли более новое ядро, — а затем действуйте по результатам. Неприемлемо только одно: не делать ни того, ни другого. - Закройте лишние слушающие порты и проверьте снаружи
Перечислите всё, что привязано не только к 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>. Это строго лучше, чем открывать порт и надеяться, что встроенная аутентификация службы достаточно надёжна, — и это шаблон, к которому стоит прибегать по умолчанию. - Зафиксируйте базовое состояние и настройте сигнализацию
Десять минут, которые окупаются лишь в тот день, когда что-то пойдёт не так, — а это именно тот день, когда восстановить всё по памяти уже не получится.
# 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) | Любого бота, подбирающего пароли, — навсегда и по самой конструкции | Любого, кто владеет вашим приватным ключом, или уязвимость в самом sshd | 5 минут, один раз | Обязательно |
| Файрвол default-deny (nftables) | Службы, о которых вы забыли, что они слушают, и любую из тех, что вы случайно установите потом | Атаки на порты, которые вы сознательно открыли | 10 минут, один раз | Обязательно |
| Автоматические обновления безопасности | Окно n-day — недели между публичным эксплойтом и вашим следующим входом | Уязвимости нулевого дня и всё, что требует перезагрузки, которую вы так и не запланировали | 5 минут плюс окно перезагрузки | Обязательно |
| Именная учётка с sudo вместо root | Опечатки в командах с полными правами и процессы, работающие от root без необходимости | Локальное повышение привилегий в руках того, кто уже внутри | 3 минуты | Оправданно |
| Базовый снимок целостности файлов (AIDE) | Незаметные правки системных бинарников, библиотек и юнит-файлов | Вообще ничего, если база остаётся на той же машине, за которой следит | 20 минут плюс хранение вне сервера | Оправданно, если сервер важен |
| Перенос SSH с порта 22 | Примерно 95% объёма лога аутентификации | Любого, кто запустит скан портов, — то есть всех, кто действительно важен | 2 минуты плюс вечер, когда вы забудете порт | Опционально, ради тихих логов |
| fail2ban / CrowdSec на sshd | Рецидивистов и трату CPU, которую они на вас навешивают | Ничего — как только парольная аутентификация отключена | 10 минут плюс реальный риск заблокировать самого себя | Пропустить для SSH; использовать для приложения |
Ответы на актуальные вопросы
Через сколько времени после развёртывания начинается сканирование?
Обычно — через считаные минуты. Сканеры, охватывающие весь интернет, непрерывно проходят всё пространство 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 с последующим прямым подключением с вашего обычного соединения полностью сводит эту оплату на нет — это разобрано в материале об ошибках, которые вас деанонимизируют.
Keep exploring
Шифрование диска VPS с помощью LUKS
Вторая половина задачи: эта страница закрывает сеть, та — защищает диск, когда машина выключена.
Ошибки анонимности, которые вас деанонимизируют
Безупречно защищённый сервер, который всё равно выдаёт всем, кому он принадлежит. Эти провалы — поведенческие, а не технические.
Настройка WireGuard на VPS
Первая служба, которую стоит спрятать за только что построенным файрволом, — и приватная дверь ко всему, что вы привязали к localhost.
Deploy your offshore server.
Выберите регион. Выберите план. Вставьте ключ. Оплатите. Следующие 47 секунд — за нами.