✻ Урок 2.7 · Тема 2: Сеть, HTTP, DNS, nginx и TLS
Файрвол и защита SSH
Содержание урока
Зачем это нужно
Сервер с публичным адресом (адресом, по которому до машины можно достучаться из интернета, урок 2.1) находят сканеры через минуты после запуска. Это программы-боты, которые перебирают адреса по всему интернету и стучатся в каждый порт. Порт это номер «двери» на машине, за которой сидит одна программа (урок 2.2). SSH (Secure Shell) это способ зайти на сервер по сети и работать в его командной строке; у него порт 22 (тоже из урока 2.2). В журнале SSH (файле, куда сервер записывает, кто и когда пытался войти) сразу появляются сотни попыток войти под именами root (главный пользователь системы с полными правами) и admin: боты угадывают, какие имена бывают чаще всего. Если порт не нужен всему миру (база данных, отладочный порт, админка: страница управления сервисом), его нельзя оставлять открытым: любой бот сможет попробовать им воспользоваться. Если вход по паролю включён, его рано или поздно подберут: бот пробует тысячи паролей в час, и слабый найдётся. Вход по ключу (длинному секретному файлу, который лежит только у тебя) перебором не взять.
Файрвол (firewall) это программа-фильтр на входе в машину: она смотрит на каждый приходящий пакет и решает, пропустить или выбросить. На работе тебя попросят «закрыть всё лишнее», ты будешь разбирать инцидент «после включения файрвола пропал SSH» (администратор закрыл и порт 22, по которому сам подключался, и остался без доступа) и объяснять, почему Docker (программа для запуска приложений в изолированных «коробках», контейнерах, урок 4.1) пробил правила, которые вроде бы всё запрещали. Всё это разберём ниже.
Шаг проекта: снаружи открыты только порты 22, 80 и 443, порт 8080 закрыт, а вход по SSH разрешён только по ключам и не под root. Код «Заметок» не меняется (остаётся app.py v3 из урока 2.4), в репозиторий добавляется файл deploy/ssh/99-notes.conf.
Что нужно знать
- Урок 1.3: пользователи и права:
sudo(выполнить команду с правами администратора), владельцы и права файлов. Пригодится для конфигов sshd (файлов настроек SSH-сервера) и папки~/.ssh. - Урок 1.4: процессы и сигналы: сигнал
SIGHUP(«перечитай настройки»). На нём держится командаreload. - Урок 1.8: systemd:
systemctl reload,journalctl -u(читать журнал одного сервиса), юниты и таймеры. - Урок 2.1: адреса и маршруты: IP-адрес, пакет, порт,
0.0.0.0и127.0.0.1, три ответа сети (refused,timed out,unreachable). - Урок 2.2: порты, TCP и SSH:
ss -tlnp, ключи ed25519, файлauthorized_keys. - Урок 2.5: nginx: nginx слушает 80 и проксирует запросы на
127.0.0.1:8080. - Урок 2.6: TLS: nginx слушает 443 и отдаёт сертификат.
Тебе нужны две машины:
- Сервер: виртуальная машина (ВМ) с Ubuntu 26.04 или 24.04 из предыдущих уроков (Multipass, Lima или OrbStack). Ниже я называю её «ВМ» и обозначаю её адрес
VM_IP, а своё имя пользователя на нейUSER. Узнать адрес:ip -4 addr show scope global. У ВМ должен быть запасной вход мимо SSH:multipass shellили консоль облака. Это страховка на случай, если ты сам себя запрёшь. - Клиент: твой ноутбук. Проверки «снаружи» делаются с него. Нужны
ssh,curlиnc(утилита netcat, проверяет, открыт ли порт). В macOS и Linux они есть. В Windowssshиcurlесть в PowerShell, а вместоncиспользуйTest-NetConnection VM_IP -Port 443.
Оговорка для WSL2: у WSL2 нет своего внешнего адреса, а ядро часто собрано без модулей фильтрации пакетов, поэтому ufw enable может выдать ошибку или ничего не фильтровать. Для этого урока бери ВМ.
Картина целиком
Представь офисное здание. На входе сидит охранник со списком: «в здание пускаем только в дверь 22 (ремонт и администраторы), в дверь 80 и в дверь 443 (посетители)». Всех остальных, кто стучится в другие двери, охранник не пускает. Это файрвол (firewall, «противопожарная стена»): он стоит на входе в машину и решает судьбу каждого пакета (порции данных).
Но даже пропущенный посетитель, который пришёл в дверь 22, попадает не в кабинеты, а к вахтёру. Вахтёр (это программа sshd, SSH-сервер: та, что на самом сервере принимает подключения по SSH, урок 2.2) не пускает по устной просьбе или по паролю, которую можно подслушать, а только по личному ключу, и не пускает под именем директора (root). Это вторая линия защиты: настройки sshd.
Вот путь пакета извне до «Заметок» и места, где его можно остановить:
flowchart TD
I["Интернет"] --> F1["1. Сетевой экран облака<br>решает до того, как пакет дошёл до ВМ (тема 6)"]
F1 --> F2["2. Файрвол ВМ: ufw<br>порты 22, 80, 443 пускает,<br>остальное молча выбрасывает"]
F2 -->|"порт 22"| SSH["3. sshd<br>только ключи, без root"]
F2 -->|"порт 443"| NG["nginx"]
NG --> APP["127.0.0.1:8080<br>«Заметки»: слушают только loopback"]
Пакет проходит три слоя защиты. Приложение слушает только loopback, поэтому снаружи до него не достать даже без файрвола.
Два слова из схемы. Сетевой экран облака это такой же файрвол, но его настраивают не на машине, а в панели облачного провайдера (компании, которая сдаёт виртуальные серверы в аренду): он стоит перед машиной, поэтому пакет, который он выбросил, до ВМ не доходит; подробно в теме 6. ufw (Uncomplicated Firewall, «несложный файрвол») это программа в Ubuntu, которой ты пишешь правила простыми командами вроде «пускай на порт 443»: сама фильтрация идёт в ядре (главной части операционной системы), а ufw лишь удобный пульт к ней. Без файрвола любая программа, которая по ошибке слушает порт на всех адресах, оказывается открытой всему интернету.
За урок ты разберёшь каждый слой: как файрвол принимает решение, чем «молча выбросить» отличается от «ответить отказом», как включить ufw и не потерять доступ, почему Docker обходит его, как настроить sshd и как читать журнал, в который стучатся боты.
Теория
Как программа принимает соединение: порт, слушающий сокет, RST
Чтобы понять, что делает файрвол, нужно знать, что именно он останавливает. Освежим главное из урока 2.1 и урока 2.2.
На одной машине работают десятки программ: sshd, nginx, «Заметки». IP-адрес приводит данные на машину, но программе нужен свой «номер квартиры». Он называется порт (port): число от 0 до 65535. По соглашению sshd живёт на 22, HTTP на 80, HTTPS на 443, наши «Заметки» на 8080.
IP-адрес это адрес дома, порт это номер квартиры, а программа это жилец. Чтобы жилец мог принять гостя, он должен быть дома и сидеть у двери. Такое состояние «сижу у двери и жду гостей» называется слушать порт (listen). Программа, которая слушает порт, держит сокет (socket): точку подключения в системе. Аналогия ломается в одном: в квартиру можно прийти и без звонка, а в порт нельзя, соединение всегда начинается с формального обмена сообщениями.
Большинство сервисов работает по протоколу TCP (Transmission Control Protocol): это правила, по которым две программы обмениваются данными надёжно, ничего не теряя. Соединение по TCP начинается с тройного рукопожатия из трёх пакетов:
sequenceDiagram
participant C as Клиент (ноутбук)
participant S as Сервер (ВМ, порт 443)
C->>S: 1. SYN: «хочу подключиться к 443»
S->>C: 2. SYN-ACK: «слушаю, давай»
C->>S: 3. ACK: «принято, начинаем»
Note over C,S: обмен данными
Соединение рождается за три шага. Если ответа на первый пакет нет, дальше дело не идёт.
Слова SYN и ACK это просто пометки в заголовке пакета (флаги): SYN значит «начинаю соединение», ACK значит «подтверждаю получение». Запоминать их не нужно, важно другое: первый пакет клиента приходит на сервер, и дальше всё зависит от того, что случится с ним.
Есть три исхода, и каждый выглядит для клиента по-разному:
flowchart TD
S["Клиент отправил SYN"] --> Q{"Что ответил сервер?"}
Q -->|"SYN-ACK"| A["Исход А: порт слушают<br>соединение есть, nc пишет «succeeded»"]
Q -->|"RST"| B["Исход Б: порт никто не слушает<br>мгновенный отказ: «Connection refused»"]
Q -->|"тишина"| V["Исход В: пакет не дошёл или его выбросили<br>клиент ждёт и сдаётся: «timed out»"]
Быстрый отказ и долгое молчание лечатся по-разному: первое про сервис, второе про файрвол или сеть.
RST (reset, «сброс») это пакет с флагом «здесь никого нет, разговор невозможен». Его посылает ядро сервера (ядро это главная часть операционной системы, которая управляет железом и сетью), когда пакет пришёл на порт без слушателя.
Вот замеры из лабораторного стенда. Клиент проверяет три порта сервера 172.17.0.2, замеряя время каждой попытки:
порт 443 (nginx слушает): Connection to 172.17.0.2 443 port [tcp/https] succeeded! 0.001 c
порт 9001 (слушателя нет): nc: connect to 172.17.0.2 port 9001 (tcp) failed: Connection refused 0.001 c
порт 9001 при файрволе: nc: connect to 172.17.0.2 port 9001 (tcp) timed out: Operation now in progress 3.01 c
Первая строка это исход А. Вторая это исход Б: ответ пришёл за одну миллисекунду, потому что сервер сразу ответил RST. Третья это исход В: клиент ждал ровно столько, сколько ему разрешили (-w 3, три секунды), а потом сдался. Самый важный вывод урока: по времени ответа можно понять, где искать поломку. Быстрый отказ значит «хост жив, порт не слушают». Долгая тишина значит «по пути пакет теряется, и чаще всего это файрвол».
Осторожно: «порт закрыт» это не одно состояние. На деле их два, и они лечатся по-разному: «никто не слушает» лечится запуском или настройкой сервиса, «пакеты выбрасывает файрвол» лечится правилом. Если перепутать, ты будешь перезапускать сервис, которому файрвол не даёт достучаться.
Прикинь сам: порт никто не слушает. Что увидит клиент: ответ или тишину?
Мгновенный ответ: ядро отправит RST, Connection refused. Тишина значит, что пакет выбросили по дороге.
Главное: три исхода: слушают (SYN-ACK), не слушают (RST, мгновенный отказ), выбросили (тишина, таймаут).
Проверь понимание:
curlк порту висит 30 секунд и падает по таймауту. Это скорее про файрвол или про остановленный сервис?
Ответ
Скорее про файрвол (или сетевой экран облака): пакеты молча теряются. Остановленный сервис на живом хосте дал бы мгновенный Connection refused, потому что ядро ответило бы RST. Исключения: хост выключен или к нему нет маршрута.
Мы выяснили, что пакет до сервиса может не дойти по дороге. Кто его останавливает и где именно это происходит, расскажет следующий раздел.
Файрвол: что это, где стоит и чем управляется
Если на сервере слушает десяток программ, а миру нужны две, остальные восемь надо спрятать. Можно настраивать каждую программу отдельно (кто-то забудет), а можно поставить один фильтр на входе. Ещё причина: программа может слушать порт «на всех адресах» по умолчанию, и ты узнаешь об этом, когда её найдёт сканер. Файрвол делает безопасность явной: разрешено только то, что ты назвал.
Файрвол похож на охранника на входе со списком разрешённых дверей. Оговорка: охранник смотрит только на «куда идёшь» и «откуда пришёл», а что ты несёшь в сумке (содержимое пакета) не проверяет. Файрвол хоста не заменяет защиту самой программы.
В Linux пакеты фильтрует ядро с помощью подсистемы netfilter. Она встроена в ядро и в нескольких точках на пути пакета спрашивает: «есть ли правила для этого пакета?». Правила записаны в цепочки (chain) по этим точкам:
flowchart LR
N["Пакет пришёл на сетевую карту"] --> D{"Кому он<br>предназначен?"}
D -->|"этой машине"| IN["INPUT"] --> P["Программа"]
D -->|"транзит через машину<br>(роутер, Docker)"| FW["FORWARD"]
P --> OUT["OUTPUT<br>пакеты, которые машина<br>отправляет сама"]
Для защиты сервера главная цепочка INPUT: через неё идёт всё, что адресовано самой машине.
Для нашей задачи важна цепочка INPUT: все пакеты, которые пришли на эту машину «для неё самой». Цепочка OUTPUT это то, что машина отправляет сама. FORWARD это транзит: пакеты, которые лишь проходят через машину дальше (так работает роутер, а ещё так работает Docker, о нём отдельный раздел ниже).
Сами правила в netfilter пишут инструменты:
iptables: старый и всё ещё повсеместный. Его синтаксис вида-A INPUT -p tcp --dport 22 -j ACCEPTсложно запомнить.nftables(командаnft): современный, он постепенно заменяетiptables. В современных Ubuntu командаiptablesчасто на самом деле работает через nftables (в выводеiptables -Vэто видно:iptables v1.8.10 (nf_tables)).ufw(Uncomplicated Firewall, «несложный файрвол»): надстройка Ubuntu. Ты пишешьufw allow 443/tcp, а он превращает это в правилаiptables. Это не отдельный файрвол: фильтрует по-прежнему одно и то же ядро. Поэтому итог работыufwможно посмотреть командойiptables -S.
Политика по умолчанию (default policy) это решение для пакета, к которому не подошло ни одно правило. Здесь два варианта, и от выбора зависит всё:
- «по умолчанию запрещено, разрешено только явно названное» (
default deny incoming). Забытый сервис на новом порту не станет дырой. Так и делают на серверах; - «по умолчанию разрешено, запрещено только явно названное». Так работают домашние компьютеры без файрвола: удобно, но забытая дверь остаётся открытой.
Для входящих (incoming) мы выбираем запрет, для исходящих (outgoing) разрешение: сервер должен сам ходить за обновлениями и в DNS.
Вот как выглядит политика цепочки INPUT после включения ufw на нашем стенде:
$ sudo iptables -S INPUT
-P INPUT DROP
-A INPUT -j ufw-before-logging-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-after-input
-A INPUT -j ufw-after-logging-input
-A INPUT -j ufw-reject-input
-A INPUT -j ufw-track-input
Разбор: -P INPUT DROP (policy) это политика: пакет по умолчанию выбрасывается. Строки -A INPUT -j ... (append, jump) это цепочка INPUT по очереди отправляет пакет в другие цепочки, которые создал ufw. Название говорит само за себя: ufw-before-input это правила «до ваших», ufw-user-input это правила, которые ты добавил командой ufw allow, ufw-after-input это правила «после». Разбирать их не нужно, важно знать, что ufw это набор цепочек iptables, а не отдельная магия.
Осторожно: ufw и iptables не два конфликтующих файрвола, это один файрвол (netfilter в ядре) и два способа его настроить. Ещё включённый ufw не блокирует то, что ты не запретил явно, если политика стоит allow. Всегда проверяй ufw status verbose и строку Default:.
Прикинь сам: какая политика по умолчанию для входящих безопаснее: «запрещено всё» или «разрешено всё»?
«Запрещено всё»: забытый сервис на новом порту не станет дырой.
Главное: фильтрует ядро (netfilter), а
ufwлишь удобная надстройка над правиламиiptables. Входящие по умолчанию запрещены, исходящие разрешены.
Проверь понимание: чем
ufwотличается отiptables?
Ответ
iptables это интерфейс к netfilter в ядре, ufw надстройка над ним с коротким синтаксисом. Правила, которые создаёт ufw, можно увидеть командой iptables -S. Фильтрует в обоих случаях одно и то же ядро.
Правило подошло пакету, и теперь нужно решить, что с ним делать. Вариантов три, и клиент видит их по-разному.
Три вердикта: allow, drop, reject
Когда правило подходит пакету, ему нужно решить: что делать? Вариантов три, и клиент видит их по-разному. Именно это различие позволяет диагностировать проблемы «по симптомам».
Допустим, ты звонишь в дверь, а за ней охранник. Что он сделает, зависит от вердикта:
allow(разрешить): дверь открылась, разговор идёт;drop(молча выбросить): за дверью никто не шевелится. Ты стоишь и не знаешь, дома ли хозяин, и уйдёшь через минуту;reject(отклонить с ответом): через дверь тебе сказали «не открою». Ты сразу понял, что вход закрыт, и ушёл.
Для drop файрвол просто выбрасывает пакет и ничего не отвечает. Для reject файрвол выбрасывает пакет, но при этом отправляет отправителю сообщение об отказе: для TCP это RST (то же, что при отсутствии слушателя), для остальных протоколов служебное сообщение ICMP «порт недоступен» (ICMP это протокол служебных сообщений сети, разобран в уроке 2.1).
В ufw команды называются allow, deny и reject. deny это drop, то есть молчаливое выбрасывание. Это значение по умолчанию для входящих: default deny incoming.
Мы прогнали три варианта для порта 9001, на котором никто не слушает, и замерили время:
| Правило ufw для 9001 | Что показал nc -zv на клиенте |
Время |
|---|---|---|
правил нет (политика deny) |
timed out: Operation now in progress |
3.01 с (упёрлись в -w 3) |
ufw allow 9001/tcp (порт разрешён, слушателя нет) |
failed: Connection refused |
0.001 с |
ufw reject 9001/tcp |
failed: Connection refused |
мгновенно |
Первая строка это drop: клиент ждёт. Вторая это ядро, которое отвечает RST, потому что файрвол пакет пропустил, а слушателя нет. Третья это файрвол, который сам отправил отказ. С клиента вторая и третья строки неотличимы. Отсюда вывод для диагностики: «быстрый отказ» это не всегда «слушателя нет», иногда это reject. Для проверки нужно смотреть на сервере: sudo ss -tlnp (слушает ли кто) и sudo ufw status numbered (нет ли REJECT).
Обрати внимание: порядок проверки такой. Пакет сначала попадает в файрвол, и только потом, если его пропустили, ядро решает, есть ли слушатель. Поэтому запрещённый порт даёт таймаут независимо от того, слушает на нём кто-то или нет.
Осторожно: deny не даёт Connection refused, он молчит. И про ping: файрвол ufw по умолчанию пропускает ping (ICMP «echo request»), поэтому «ping проходит» ничего не говорит о том, открыты ли порты. На стенде при включённом ufw и запрещённых портах ping проходил без потерь.
Прикинь сам: клиент ждёт ответ ровно 3 секунды, потом
timed out. Этоdropилиreject?
drop: файрвол молча выбросил пакет. При reject отказ пришёл бы сразу.
Главное:
allowпускает,dropмолчит,rejectотвечает отказом.ufw denyэто drop.
Проверь понимание: ты запустил
nc -zv host 8443и сразу получилConnection refused. Значит ли это, что на сервере точно никто не слушает порт 8443?
Ответ
Не обязательно. Либо порт открыт в файрволе и слушателя нет, либо стоит правило reject, которое тоже даёт быстрый отказ. Отличить можно только на самом сервере: ss -tlnp покажет, слушает ли кто-то порт, а ufw status numbered покажет, нет ли там REJECT.
Вердикты понятны, но правил в файрволе обычно много, и они могут противоречить друг другу. Как файрвол выбирает между ними и что он помнит о соединениях, смотрим дальше.
Правила, их порядок и «память» файрвола о соединениях
Два правила могут противоречить друг другу: «пускать всех на 443» и «не пускать этот адрес на 443». Кто победит? И есть вторая загадка: если ты включил файрвол, который запрещает всё входящее, почему уже открытая SSH-сессия не умирает сразу?
Список правил это как инструкция для охранника, которую он читает сверху вниз и действует по первой подходящей строке. Дочитывать до конца не нужно. Аналогия перестаёт работать в одном: у охранника нет памяти, а у файрвола есть, об этом ниже.
Правила хранятся списком по порядку добавления. Пакет сравнивается с правилами сверху вниз, и срабатывает первое подходящее. Поэтому узкий запрет должен стоять выше общего разрешения. Если ничего не подошло, действует политика по умолчанию.
Вторая часть: файрвол с учётом состояния (stateful). Ядро запоминает каждое установленное соединение в таблице отслеживания соединений (connection tracking, conntrack) и помечает пакеты состояниями: NEW (первый пакет нового соединения), ESTABLISHED (пакет уже открытого соединения), RELATED (связанный, например служебная ошибка). Самое первое правило ufw разрешает всё ESTABLISHED,RELATED. Благодаря этому не нужно отдельно открывать порты для ответов на твои же исходящие запросы, и уже открытая сессия SSH переживает включение файрвола.
Мы вставили запрет для одного клиента выше общего разрешения:
$ sudo ufw insert 1 deny from 172.17.0.13 to any port 443 proto tcp
Rule inserted
$ sudo ufw status numbered
To Action From
-- ------ ----
[ 1] 443/tcp DENY IN 172.17.0.13
[ 2] 22/tcp LIMIT IN Anywhere # ssh limit
[ 3] 80/tcp ALLOW IN Anywhere # http
[ 4] 443/tcp ALLOW IN Anywhere # https
Клиент 172.17.0.13 на 443 попал в правило 1 и получил таймаут, хотя строка 4 разрешает 443 «всем». Порт 80 для него остался открыт (правило 3). Если бы DENY стоял под ALLOW, он не сработал бы никогда: разрешение поймало бы пакет раньше.
Теперь про «память». Вот как первые правила ufw выглядят в iptables:
$ sudo iptables -S ufw-before-input | head -3
-N ufw-before-input
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Разбор: -i lo -j ACCEPT это «всё, что пришло на интерфейс lo (loopback, урок 2.1), пропускать». Поэтому «Заметки» на 127.0.0.1:8080 всегда доступны с самой машины и nginx достучится до них при любом файрволе. Вторая строка: --ctstate RELATED,ESTABLISHED -j ACCEPT это «пакеты уже установленных соединений пропускать». Она и спасает твою SSH-сессию.
Обратная сторона: соединения с самой машины на собственный внешний адрес тоже идут через lo. Поэтому проверять доступность порта с самого сервера бессмысленно: файрвол его не увидит. На стенде nc с сервера на его же адрес дал succeeded для порта, который снаружи был закрыт. Проверяй порты только с другой машины.
Осторожно: правила не «действуют на запущенные сессии», только на новые соединения. Из-за этого удалить правило для 22 и включить файрвол очень опасно: твоя текущая сессия ещё жива, и создаётся ложное ощущение, что всё нормально. Проблема вылезет, когда сессия оборвётся.
Прикинь сам: правило «запретить 443 для одного клиента» стоит ниже общего «разрешить 443». Этот клиент пройдёт?
Да: срабатывает первое подходящее правило сверху, и разрешение его пропустит. Узкий запрет ставят выше.
Главное: правила читаются сверху вниз до первого совпадения, а уже установленные соединения файрвол помнит (conntrack).
Проверь понимание: ты выполнил
ufw enableв SSH-сессии без правила для 22. Сессия жива. Почему, и что случится дальше?
Ответ
Пакеты установленного соединения относятся к состоянию ESTABLISHED, а stateful-файрвол их пропускает. Новые соединения на 22 отбрасываются. Как только эта сессия оборвётся (сеть моргнула, закрыл ноутбук), зайти станет нельзя, только через консоль ВМ.
Принципы ясны: порядок, вердикты, память о соединениях. Теперь запишем их командами ufw и включим файрвол так, чтобы не потерять собственную SSH-сессию.
ufw: язык правил и безопасное включение
ufw уже знаешь как «надстройку над iptables». Теперь его команды по порядку: что каждая делает и чем опасна.
ufw это как переводчик: ты говоришь по-человечески «пусти на 443», а он записывает это на языке ядра. Оговорка: переводчик применяет правила мгновенно, без «сохранить и применить», поэтому ошибка бьёт сразу.
Основные команды:
ufw default deny incomingиufw default allow outgoing: политика по умолчанию (что делать со всем, что не названо явно);ufw allow 443/tcp: открыть порт 443 для TCP. Запись «порт/протокол»; без протокола откроются и TCP, и UDP. К правилу можно добавитьcomment 'https': пояснение, которое видно вstatus;ufw allow OpenSSH: открыть по имени профиля. Профили описывают типичные сервисы, список показываетufw app list(на стенде их четыре:Nginx Full,Nginx HTTP,Nginx HTTPS,OpenSSH). Мы будем писать порты, так нагляднее;ufw allow from 203.0.113.10 to any port 22 proto tcp: открыть порт только для одного адреса. Так можно ограничить SSH известными адресами;ufw limit 22/tcp: разрешить, но отбивать частые подключения с одного адреса (подробнее ниже);ufw status numbered: список правил с номерами.ufw status verboseдобавляет политики.ufw delete <номер>(илиufw delete allow 443/tcp) удаляет правило, при этомufwспросит подтверждениеProceed with operation (y|n)?, чтобы обойти вопрос, добавь--force;ufw enable,ufw disable,ufw reload: включить, выключить, перечитать правила.
Обрати внимание, что каждое правило ufw создаёт дважды: для IPv4 и для IPv6 (в списке вторая строка с пометкой (v6)). IPv6 это новая версия IP-адресов, в выводе ip a ты видел их как fe80::... (урок 2.1). Если бы ufw открыл порт только для IPv4, сервис оставался бы открытым всему миру по IPv6.
Как limit устроен внутри. Вот настоящие правила, которые ufw создал для ufw limit 22/tcp:
-A ufw-user-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set ...
-A ufw-user-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 30 --hitcount 6 ... -j ufw-user-limit
-A ufw-user-input -p tcp --dport 22 -j ufw-user-limit-accept
...
-A ufw-user-limit -j REJECT --reject-with icmp-port-unreachable
Читаем по строкам. Первая: для каждого нового соединения на 22 запомнить адрес отправителя и время. Вторая: если с этого адреса за 30 секунд (--seconds 30) пришло шесть и больше попыток (--hitcount 6), отправить пакет в цепочку ufw-user-limit. Третья: иначе принять. Последняя строка показывает важное: цепочка ufw-user-limit отклоняет (REJECT), а не молча выбрасывает. Значит, под limit ты получишь не таймаут, а мгновенный Connection refused (мы это проверили, ниже в практике).
Безопасное включение. ufw enable применяется мгновенно, поэтому порядок такой:
- сначала правило для 22, потом
enable; - на удалённом сервере ещё и отложенный откат: ты заранее просишь систему через 5 минут выключить файрвол, и отменяешь просьбу, только убедившись, что вход работает;
- проверка второй SSH-сессией, первую не закрываем.
Отложенный откат делается через systemd. Помнишь юниты из урока 1.8? Кроме .service бывают .timer: юнит-таймер, который запускает другой юнит по расписанию. Команда systemd-run --on-active=5min --unit=ufw-rollback /usr/sbin/ufw disable создаёт временный таймер ufw-rollback.timer, который через 5 минут запустит ufw disable. Отменить: systemctl stop ufw-rollback.timer. Если ты запрёшь себя и сессия потеряется, через 5 минут файрвол выключится сам и ты войдёшь.
Осторожно: если открыл порт в ufw, это ещё не значит, что сервис доступен. ufw только разрешает пакеты. Если сервис не запущен или слушает только 127.0.0.1, снаружи будет Connection refused (а не таймаут). И обратное: «сервис слушает 0.0.0.0, значит он открыт»: без правила в ufw пакеты не дойдут.
Прикинь сам: что страшнее при включении
ufwпо SSH: забыть правило для 80 или для 22?
Для 22: новые подключения по SSH перестанут проходить, и доступ к серверу потеряется. Сначала allow 22, потом enable.
Главное: сначала правила и разрешение SSH, затем
ufw enable.ufw limitпритормаживает перебор.
Проверь понимание: для чего нужен
ufw limit 22/tcpи что клиент увидит при превышении лимита?
Ответ
Он разрешает SSH, но если с одного адреса за 30 секунд приходит шесть и больше новых подключений, следующие отклоняются. Клиент увидит мгновенный Connection refused (правило использует REJECT, а не молчаливый DROP). Через 30 секунд после последней попытки доступ возвращается. Это притормаживает перебор, но не заменяет ключи.
Правила введены и работают. Но что с ними станет после перезагрузки сервера, зависит от того, где они хранятся, и об этом следующий раздел.
Где ufw хранит правила и что происходит после перезагрузки
Правила, которые ты ввёл, живут в памяти ядра, а память после перезагрузки пуста. Если бы ufw не сохранял их на диск, каждая перезагрузка возвращала бы сервер без защиты (или, наоборот, отрезала бы тебя от него).
Правила в памяти ядра похожи на список для охранника, который ты пишешь на доске: по утрам доску стирают. Чтобы список пережил ночь, его нужно переписать в журнал, а утром охранник сам перенесёт его на доску. Оговорка: ufw переписывает в журнал автоматически при каждой команде, вручную «сохранять» ничего не нужно.
Шаг за шагом это происходит так:
- Ты выполняешь
ufw allow 443/tcp. ufwдописывает правило в файлы в каталоге/etc/ufw/(для IPv4 этоuser.rules, для IPv6user6.rules) и одновременно применяет его к ядру.- Команда
ufw enableвключает службуufw.serviceв автозапуск (в выводе ты виделFirewall is active and enabled on system startup, то есть «включён и будет включаться при загрузке»). - После перезагрузки systemd запускает
ufw.service, тот читает файлы из/etc/ufw/и заново загружает правила в ядро.
Сам iptables -S показывает только текущее состояние ядра и после перезагрузки для правил, поставленных вручную, будет пуст. Поэтому правила «на лету» через голый iptables пропадают, а через ufw остаются.
Файлы можно открыть глазами: sudo ls /etc/ufw/ покажет среди прочего ufw.conf (там строка ENABLED=yes, включён ли файрвол при загрузке), user.rules и user6.rules (твои правила в формате iptables). А в /etc/default/ufw лежат политики по умолчанию и строка IPV6=yes, которая отвечает за то, создаются ли правила (v6). Если в ufw.conf стоит ENABLED=no, то ufw status покажет Status: inactive, и правила есть в файлах, но не загружены. Именно это делает ufw disable: правила остаются на диске, файрвол выключается. Поэтому отложенный откат ufw disable из этого урока безопасен: включить обратно можно одной командой, ничего не пропадёт.
Что это значит на практике. Если ты перед перезагрузкой проверил ufw status и там нужные правила, они переживут перезагрузку. Если сервис зависит от порядка запуска (например, nginx стартует раньше ufw), окно в несколько секунд без защиты обычно невелико: ufw.service запускается на очень раннем этапе загрузки.
Осторожно: ufw disable не стирает правила, а только выключает фильтрацию. Стирает правила ufw reset (после него нужно начинать с нуля, поэтому в боевой работе эту команду без необходимости не запускают). И вручную добавленные iptables-правила не переживут перезагрузку, если их не сохранили отдельным инструментом.
Прикинь сам: после перезагрузки сервера правила ufw пропадут?
Нет, ufw хранит их в файлах и применяет заново при старте, если он включён.
Главное: правила живут в файлах,
ufw disableне стирает их, а лишь выключает.
Проверь понимание: ты выполнил
sudo ufw disable, а через часsudo ufw enable. Придётся ли заново вводить всеallow?
Ответ
Нет. disable лишь выключает файрвол, а правила остаются в /etc/ufw/user.rules и user6.rules. enable снова загрузит их в ядро. Стирает правила только ufw reset.
С правилами и их хранением разобрались. Теперь о ловушке, из-за которой порт кажется закрытым, а на деле открыт: Docker.
Docker обходит ufw
Это самая частая причина неприятных инцидентов: администратор закрыл порт базы в ufw, проверил ufw status, а сканер видит базу открытой.
Охранник стоит у главного входа, а Docker пробивает отдельную дверь прямо в нужную комнату, минуя охранника. Оговорка: Docker делает это не со зла, а чтобы порты контейнеров «просто работали» (тему Docker ты пройдёшь в теме 4).
Когда ты запускаешь контейнер с ключом -p 5432:5432 («опубликовать порт 5432 хоста на порт 5432 контейнера»), Docker добавляет правило в таблицу трансляции адресов (nat, идея NAT из урока 2.1). Оно перенаправляет пакеты, пришедшие на порт хоста, на внутренний адрес контейнера. Такой пакет уже не считается пришедшим «для этой машины», теперь он транзитный, идёт через цепочку FORWARD, а не через INPUT. А правила ufw стоят в INPUT. Поэтому запретительные правила ufw до него не доходят.
На хосте с Docker мы запустили docker run -p 5432:8000 python:3.12-alpine ... (снаружи порт 5432, внутри контейнера 8000) и посмотрели таблицу nat:
$ sudo iptables -t nat -S DOCKER
-N DOCKER
-A DOCKER -p tcp -m tcp --dport 5432 -j DNAT --to-destination 172.17.0.18:8000
Разбор: -t nat это таблица трансляции, DNAT (Destination NAT) подменяет адрес получателя, --dport 5432 это порт снаружи, --to-destination 172.17.0.18:8000 это адрес контейнера и его порт. docker ps показывает то же самое как 0.0.0.0:5432->8000/tcp: слева порт хоста на всех адресах. Если у порта стоит 0.0.0.0, он открыт всем сетевым картам, и ufw его не закроет.
Три защиты:
- публиковать порт только на localhost:
-p 127.0.0.1:5432:5432(доступен только с самой машины); - не публиковать порт вообще: контейнеры общаются между собой во внутренней сети Docker Compose (тема 4);
- ставить жёсткие правила в цепочку
DOCKER-USER: Docker гарантирует, что её правила проверяются раньше его собственных.
Осторожно: одного ufw status недостаточно. Проверять нужно то, что видит внешний мир: docker ps (какие порты опубликованы), sudo iptables -t nat -S DOCKER и сканирование порта с другой машины.
Прикинь сам: опубликовал порт контейнера
-p 5432:5432. Остановит ли егоufw?
Нет: Docker добавляет свои правила в обход ufw, порт будет виден снаружи.
Главное: Docker обходит
ufw, поэтому проверяйiptablesи привязывай порт к127.0.0.1.
Проверь понимание:
ufw statusне содержит правила для 5432, но порт отвечает снаружи. Что проверишь первым?
Ответ
docker ps (какие порты опубликованы и на каком адресе) и sudo iptables -t nat -S DOCKER. Скорее всего, порт открыт правилом Docker, а не ufw.
Одна защита может сломаться незаметно, как это случилось с Docker. Поэтому порт закрывают сразу несколькими способами, и об этом следующий раздел.
Защита слоями: почему порт 8080 закрыт дважды
Одна защита ломается: кто-то ошибся в правиле, забыл про IPv6, поставил Docker. Поэтому важное закрывают несколькими независимыми слоями: если один прорван, второй держит.
Слои работают так же, как замок на подъезде и замок на двери квартиры.
Для «Заметок» слоёв три:
- Сетевой экран облака (группы безопасности, тема 6): фильтр вне ВМ, пакет до неё вообще не дойдёт. Не заменяет файрвол на самой машине: в облаке правила обычно шире, а ВМ могут пересоздавать.
- Файрвол ВМ (
ufw): порт 8080 не разрешён, значит закрыт. - Привязка сервиса: в
/etc/notes/notes.envзаписаноHOST=127.0.0.1(урок 2.1), и «Заметки» слушают только loopback. Пакет с внешнего адреса не может прийти на127.0.0.1в принципе.
Проверим с клиента: curl http://VM_IP:8080/healthz завершается ошибкой curl: (28) Connection timed out after 5003 milliseconds (порт закрыт файрволом). А curl -k https://VM_IP/healthz возвращает 200: nginx на 443 принимает запрос и передаёт его «Заметкам» изнутри, через loopback, который файрволу не мешает (см. -i lo -j ACCEPT выше). Даже если бы файрвол был выключен, ответ на порту 8080 с адреса VM_IP был бы Connection refused (слушателя на этом адресе нет).
Осторожно: не думай, что «раз слой 3 защищает, файрвол не нужен». Слой 3 работает, пока в notes.env стоит 127.0.0.1. Один неверный HOST=0.0.0.0 (как в эксперименте урока 2.1) убирает его. Файрвол держит порт закрытым независимо от настроек сервиса.
Прикинь сам: если один слой защиты порта 8080 даст сбой, останется ли порт закрытым?
Да, пока работает хотя бы один из остальных слоёв.
Главное: важное закрывают несколькими слоями: сетевой экран облака, файрвол ВМ и привязка сервиса к
127.0.0.1.
Проверь понимание: какие слои закрывают порт 8080 на нашем сервере?
Ответ
Файрвол ВМ (порт 8080 не разрешён) и привязка сервиса к 127.0.0.1. В облаке добавляется ещё сетевой экран. Каждый слой достаточен сам по себе, вместе они страхуют друг друга от ошибок.
Порты закрыты в несколько слоёв, но один порт должен оставаться открытым: SSH, главный вход на сервер. Как его настроить так, чтобы он не стал слабым местом, смотрим дальше.
SSH-сервер: конфигурация и правило «побеждает первое значение»
SSH это главный вход на сервер, поэтому его настройки самые важные. И самая частая проблема: ты правишь конфиг, а поведение не меняется.
Конфигурация sshd читается как правила внутреннего распорядка, сложенные из нескольких листов. Если на первом листе написано «вход по паролю разрешён», а на последнем «запрещён», действует первый, потому что вахтёр читает листы по порядку и останавливается на первом ответе. Оговорка: обычно в документах побеждает последний лист, а sshd делает наоборот.
SSH (Secure Shell) это протокол защищённого удалённого входа, ты им пользовался в уроке 2.2. Программа-сервер называется sshd (буква d от daemon, «демон», фоновая служба). Она читает основной файл /etc/ssh/sshd_config. В Ubuntu в самом начале этого файла стоит строка Include /etc/ssh/sshd_config.d/*.conf, то есть «подключить все файлы .conf из этой папки». Файлы подключаются по алфавиту, их удобно использовать как «дополнения»: не трогать основной файл, а класть рядом свой.
Правило sshd: для каждой опции действует первое найденное значение. Вот что из этого следует, если в sshd_config.d два файла:
/etc/ssh/sshd_config.d/50-cloud-init.conf PasswordAuthentication yes <- читается первым, побеждает
/etc/ssh/sshd_config.d/99-notes.conf PasswordAuthentication no <- проигнорировано
50-cloud-init.conf идёт раньше 99-notes.conf (5 меньше 9). Файл 50-cloud-init.conf создаёт cloud-init: программа, которая настраивает облачную ВМ при первом запуске (у неё бывают и другие имена файлов, например 60-cloudimg-settings.conf). Поэтому проверять нужно не файл, а итог. Для этого две команды, их путают:
sshd -t(test): проверяет только синтаксис файлов. Молчит, если всё в порядке, и печатает ошибку, если есть опечатка. Итоговые значения не показывает;sshd -T(большая T, extended test): печатает итоговые значения всех опций, которые sshd реально применит. Нужны права администратора, поэтомуsudo sshd -T. Вывод в нижнем регистре, по строке на опцию:passwordauthentication no.
Обрати внимание на «мотор» sshd в Ubuntu 24.04 и новее. Ubuntu запускает sshd не сразу, а по требованию: юнит ssh.socket слушает порт 22 сам, а когда приходит первое подключение, запускает ssh.service. Поэтому на свежей ВМ, к которой ещё никто не подключался, sudo sshd -T может ответить Missing privilege separation directory: /run/sshd. Причина в том, что каталог /run/sshd sshd создаёт при запуске, а он ещё не запускался. Если ты работаешь по SSH, подключение уже случилось, и всё работает. Заодно это объясняет странность в ss -tlnp: порт 22 держат сразу два процесса, systemd (сокет) и sshd.
Так выглядит расхождение «файл говорит no, sshd делает yes» на стенде (Ubuntu 24.04, OpenSSH 9.6):
$ sudo sshd -T | grep -Ei '^(passwordauthentication|permitrootlogin|pubkeyauthentication) '
permitrootlogin no
pubkeyauthentication yes
passwordauthentication yes <- мы положили в 99-notes.conf «no», а итог «yes»
$ grep -rn -i 'PasswordAuthentication' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
/etc/ssh/sshd_config:66:#PasswordAuthentication yes
/etc/ssh/sshd_config.d/50-cloud-init.conf:3:PasswordAuthentication yes
/etc/ssh/sshd_config.d/99-notes.conf:2:PasswordAuthentication no
Как читать: grep -rn -i ищет слово во всех файлах (-r по папке, -n с номером строки, -i без учёта регистра). Строки с # в начале это комментарии, sshd их не читает. Виновник найден: 50-cloud-init.conf:3. После того как мы закомментировали его строку, sshd -T показал passwordauthentication no.
Ещё одна деталь: опция permitrootlogin без правок показывает without-password (Ubuntu 24.04) или prohibit-password (Ubuntu 26.04). Это одно и то же значение под двумя названиями: «root можно пускать, но только по ключу».
Осторожно: после правки конфига недостаточно sshd -t, он подтверждает лишь, что опечаток нет. И правка не вступает в силу сама: sshd читает конфигурацию при запуске и по команде перечитать (об этом дальше).
Прикинь сам: в
50-cloud-init.confстоитyes, в99-notes.confстоитno. Какое значение действует?
yes: у sshd побеждает первое найденное значение, а 50- читается раньше 99-. Итог видно в sshd -T.
Главное: итоговую настройку смотрят через
sshd -T, а не по одному файлу.
Проверь понимание: ты положил
PasswordAuthentication noв99-notes.conf, но пароль всё равно принимается. Где искать причину и чем проверить?
Ответ
Более ранний файл в sshd_config.d (например 50-cloud-init.conf) или основной sshd_config уже задал yes, а побеждает первое значение. Проверка: sudo sshd -T | grep -i passwordauthentication и grep -ri PasswordAuthentication /etc/ssh/. Второй возможный виновник: sshd не перечитал конфигурацию после правки.
Мы знаем, как sshd читает конфиг. Теперь выберем в нём три настройки, которые закрывают почти все типичные атаки.
Три настройки SSH, которые закрывают почти всё
Пароль можно угадать перебором: боты пробуют тысячи вариантов в час. Ключ угадать нельзя. Поэтому если пароли отключить совсем, подбирать станет нечего.
Пароль это как код от двери на бумажке: его можно подсмотреть или перебрать. Ключ это физический ключ, который есть только у тебя: его нужно украсть, а не угадать. Оговорка: ключ можно потерять или украсть вместе с ноутбуком, поэтому закрытый ключ защищают паролем-фразой и правами 600.
Вход по ключу ты настроил в уроке 2.2: пара ключей id_ed25519 (закрытый остаётся у тебя) и id_ed25519.pub (открытый лежит на сервере в ~/.ssh/authorized_keys). Сервер проверяет, что у клиента есть закрытая часть, не получая её. Минимальный набор для нашего сервера:
PasswordAuthentication no: пароль не принимается вообще, подбирать нечего;PermitRootLogin no:rootпо SSH не входит. Имяrootесть на каждой машине, поэтому боты пробуют его первым. Работать нужно под своим пользователем и повышать права черезsudo(урок 1.3);PubkeyAuthentication yes: вход по ключу включён. Он включён по умолчанию, но мы фиксируем его явно, чтобы случайная строка в другом файле не смогла его выключить незаметно.
Порядок безопасной правки. Ошибка в конфиге SSH может отрезать тебя от сервера, поэтому порядок жёсткий:
- Перед правкой убедись, что вход по ключу работает. Иначе, отключив пароль, ты останешься совсем без входа.
- Внеси правку и проверь синтаксис:
sudo sshd -t. Пока он не молчит, дальше нельзя. - Примени конфигурацию командой
reload(sudo systemctl reload ssh), а неrestart.reloadпосылает sshd сигналSIGHUP(урок 1.4): «перечитай настройки». Главный процесс не умирает, а уже установленные сессии не рвутся.restartостановил бы sshd и запустил заново: при ошибке в конфиге он не поднимется, и ты потеряешь доступ. - Не закрывая первую сессию, открой вторую и убедись, что вход по ключу работает. Только после этого закрывай первую. Старая сессия это твоя страховка.
После настройки клиент делает три попытки входа:
$ ssh -o PasswordAuthentication=no ubuntu@172.17.0.2 'echo вход по ключу работает'
вход по ключу работает
$ ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password ubuntu@172.17.0.2
ubuntu@172.17.0.2: Permission denied (publickey).
$ ssh -o PasswordAuthentication=no root@172.17.0.2
root@172.17.0.2: Permission denied (publickey).
Как читать: в первой строке ключ сработал. Во второй мы заставили клиента пробовать только пароль, а сервер отказал, причём слова в скобках (publickey) означают «сервер согласен принимать только ключ». Число в скобках это список способов, которые сервер ещё готов принять. В третьей мы вошли бы под root (и ключ прошёл бы, будь он там), но сервер отказывает: PermitRootLogin no.
Осторожно: «сменить порт SSH на 2222 и всё» не работает. Смена порта убирает шум ботов, но не защищает от целенаправленного сканирования (сканер найдёт открытые порты за минуты). Настоящая защита это ключи и запрет root. И Permission denied (publickey) не проблема сети: это значит, что сервер жив и отвечает, но аутентификация не прошла. Если в скобках пусто (Permission denied ().), сервер вообще не предлагает способов входа, и это признак выключенных ключей и паролей одновременно (пример в «Сломай и почини»).
Прикинь сам: отключил пароль, а вход по ключу не проверил. Что может случиться?
Ты закроешь себе единственный вход на сервер, и вернуть его можно будет только через консоль провайдера.
Главное: три настройки закрывают почти всё: вход по ключу, запрет пароля и запрет входа root. Ключ проверяй до отключения пароля.
Проверь понимание: зачем перед отключением пароля нужно проверить вход по ключу, а не наоборот?
Ответ
Если отключить пароль, а ключ не работает, войти станет нельзя (только через консоль ВМ). Проверка ключа заранее стоит секунду, а восстановление доступа минуты или часы. Поэтому сначала убеждаешься, что запасной способ входа есть, и только потом закрываешь основной.
Настройки закрывают вход паролем, но боты всё равно будут стучаться. Как отличить обычный шум от настоящей угрозы и как его ограничить, смотрим в последнем разделе.
Шум в журнале: сканеры, fail2ban и limit
Даже когда пароли выключены, боты продолжают стучаться, и журнал засоряется тысячами строк. Нужно уметь отличать фоновый шум от настоящей угрозы.
Вспомни: у любого подъезда кто-нибудь подёргает ручку двери. Это шум. Тревожно другое: кто-то вошёл, хотя ключа у него быть не должно.
Журнал sshd читают командой journalctl -u ssh (-u значит «только этот юнит», урок 1.8). В нём три вида строк, которые нужно узнавать:
Invalid user admin from 203.0.113.7 port 51522: кто-то пробует несуществующее имя. Это шум сканеров, пугаться нечего;Failed password for invalid user admin ...илиFailed password for root ...: попытка входа по паролю не удалась. Тоже шум, если пароли выключены (подобрать нечего);Accepted publickey for ubuntu from 198.51.100.9 port 34162 ssh2: успешный вход по ключу. Вот эти строки читай внимательно: тот ли пользователь, тот ли адрес.
Способы снизить шум, ни один не заменяет ключи:
ufw limit 22/tcp: описан выше, режет частые подключения с одного адреса;- fail2ban: отдельная программа, которая читает журнал, считает неудачные входы и на время банит адрес правилом файрвола. Мы его не ставим, для нашего сервера хватит ключей и
limit, здесь только обзор; - доступ к 22 только с известных адресов (
ufw allow from ...) или через бастион (bastion): промежуточный сервер, единственная машина, у которой открыт SSH наружу, а с неё уже ходят на остальные (ключssh -J: «пройди к цели через этот промежуточный сервер»; устройство бастиона и пример разобраны в уроке 2.2, раздел про ключ-J); - смена порта: убирает шум ботов, но не защищает. Ещё одна особенность Ubuntu 24.04 и новее: раз порт держит
ssh.socket, то менять его нужно и в юните сокета, а не только вsshd_config. Мы порт не меняем.
Мы устроили неудачный вход с клиента под несуществующим именем и нашли его в журнале сервера:
Sep 30 11:54:23 5a4d47f76e01 sshd[1142]: Invalid user nosuchuser from 172.17.0.13 port 54492
Sep 30 11:54:24 5a4d47f76e01 sshd[1142]: Failed password for invalid user nosuchuser from 172.17.0.13 port 54492 ssh2
Sep 30 11:54:26 5a4d47f76e01 sshd[1142]: Connection closed by invalid user nosuchuser 172.17.0.13 port 54492 [preauth]
Разбор: в начале строки время, имя машины (5a4d47f76e01) и sshd[1142], где 1142 это номер процесса (PID), обслуживающего это подключение. Дальше сообщение. [preauth] значит «до завершения аутентификации»: клиент так и не вошёл. Три строки с одним PID это один и тот же неудачный вход.
Осторожно: тысячи Failed password не взлом, это обычный шум. Взлом это успешный Accepted ... для неизвестного пользователя или источника, либо изменение authorized_keys.
Прикинь сам: пароли отключены, а в журнале SSH тысячи неудачных попыток. Это взлом?
Нет, это шум от сканеров: без пароля перебор бесполезен. Но он засоряет журнал, и с ним помогает fail2ban или ufw limit.
Главное: сканеры стучатся в любой SSH, шум снижают блокировкой повторных ошибок, а не отключением журнала.
Проверь понимание: зачем
fail2ban, если пароли отключены?
Ответ
Он не защищает от взлома (подбирать нечего), а снижает нагрузку и шум: боты банятся на время, журнал остаётся читаемым. Главная защита остаётся прежней: ключи и запрет root.
Практика
Задания выполняются на ВМ (сервер) и на ноутбуке (клиент), в каждом шаге сказано, где. Значения VM_IP и USER подставь свои. Настоящие выводы ниже сняты на стенде: Ubuntu 24.04 в Docker (сервер 172.17.0.2, «ноутбук» 172.17.0.13, пользователь ubuntu). У тебя адреса, PID и время будут другими. Тексты nc на macOS отличаются от Linux: там вместо timed out: Operation now in progress пишется failed: Operation timed out, а вместо connect to пишется connectx to.
Задание 1. ufw: запретить всё, разрешить нужное, включить безопасно
Цель: включить файрвол с правилами «22, 80, 443» и не потерять доступ.
Предскажи: что покажет ufw status про порт 8080 после default deny incoming и allow для 22, 80, 443? Что было бы с уже открытой SSH-сессией, если бы ты забыл про 22?
Ответ
Порта 8080 в списке не будет (входящие на него запрещены политикой). Без правила для 22 текущая сессия осталась бы, а новые входы нет (см. теорию).
Шаги (на ВМ):
-
Установи
ufw, если его нет, и посмотри статус.apt install -yставит пакет и отвечает «да» на вопрос об установке.ufw status verboseпечатает статус с политиками:sudo apt install -y ufw sudo ufw status verboseОжидаемый вывод второй команды (файрвол ещё выключен):
Status: inactive -
Поставь страховку: через 5 минут файрвол сам выключится. Разбор команды:
systemd-runсоздаёт временный юнит,--on-active=5minэто «запусти через 5 минут после создания»,--unit=ufw-rollbackэто его имя, а всё после это команда, которую нужно запустить (ufw disable, полный путь обязателен, потому что systemd не ищет команды поPATH, урок 1.8):sudo systemd-run --on-active=5min --unit=ufw-rollback /usr/sbin/ufw disableRunning timer as unit: ufw-rollback.timer Will run service as unit: ufw-rollback.service -
Задай политику и правила. Правило для SSH идёт первым.
commentэто подпись, которая видна в списке:sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp comment 'ssh' sudo ufw allow 80/tcp comment 'http' sudo ufw allow 443/tcp comment 'https' sudo ufw enableDefault incoming policy changed to 'deny' (be sure to update your rules accordingly) Default outgoing policy changed to 'allow' (be sure to update your rules accordingly) Rules updated Rules updated (v6) Rules updated Rules updated (v6) Rules updated Rules updated (v6) Firewall is active and enabled on system startupЕсли после
enableпоявился вопросCommand may disrupt existing ssh connections. Proceed with operation (y|n)?, ответьy: правило для 22 уже есть. -
Открой вторую SSH-сессию с ноутбука:
ssh USER@VM_IP. Если вход прошёл, отмени откат и посмотри правила:sudo systemctl stop ufw-rollback.timer sudo ufw status numbered
Что должно получиться:
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere # ssh
[ 2] 80/tcp ALLOW IN Anywhere # http
[ 3] 443/tcp ALLOW IN Anywhere # https
[ 4] 22/tcp (v6) ALLOW IN Anywhere (v6) # ssh
[ 5] 80/tcp (v6) ALLOW IN Anywhere (v6) # http
[ 6] 443/tcp (v6) ALLOW IN Anywhere (v6) # https
Как читать вывод:
Status: active: файрвол включён.Toэто порт назначения,Actionэто вердикт (ALLOW IN: пускать входящие),Fromэто от кого (Anywhere: откуда угодно). Последняя колонка# sshэто твой комментарий.- Номера в квадратных скобках нужны для
ufw delete <номер>. - Ты видишь шесть строк: каждое правило записано дважды, для IPv4 и IPv6. Строки с
(v6)относятся к IPv6. - Порта 8080 в списке нет, значит, он запрещён политикой
deny. systemctl stop ufw-rollback.timerничего не печатает при успехе. Проверить, что таймера больше нет:systemctl list-timers ufw-rollback.timerпокажет0 timers listed.
Объясни себе:
- Зачем правила дублируются с
(v6)? - Почему откат ставится ДО изменений, а отменяется ПОСЛЕ проверки второй сессией?
- Что было бы с откатом, если бы ты закрыл ВМ на 6 минут?
Типичные ошибки:
ERROR: problem running ufw-initилиip6tables-restore: line 4 failed: у ядра нет нужных модулей (часто WSL2) или отключён IPv6, а в/etc/default/ufwстоитIPV6=yes. Используй ВМ или поставьIPV6=noи повториsudo ufw reload.sudo: ufw: command not found: пакет не установлен:sudo apt install -y ufw.Failed to start transient timer unit: Unit ufw-rollback.timer was already loaded or has a fragment file.: откат уже стоит с прошлой попытки. Выполниsudo systemctl stop ufw-rollback.timerи повтори шаг 2.
Задание 2. Проверить, что видно снаружи
Цель: отличить «закрыт файрволом» от «никто не слушает» и убедиться, что 8080 закрыт.
Команда nc (netcat) умеет проверять порт. Разбор флагов: -z значит «только проверь, что порт открывается, данных не посылай», -v (verbose) просит написать результат, -w 3 (wait) ограничивает ожидание тремя секундами. Без -w nc ждал бы дольше минуты.
Предскажи: на ВМ запущен python3 -m http.server 9000 (слушает все интерфейсы). Что покажет nc -zv VM_IP 9000 с ноутбука при включённом ufw? Что покажет то же для порта 9001, где никто не слушает? Чем ответы отличаются по времени?
Ответ
Оба порта запрещены политикой, поэтому оба дадут таймаут: файрвол роняет пакеты раньше, чем ядро решит, есть ли слушатель. Connection refused появился бы, только если бы порт был открыт в ufw, а слушателя не было.
Шаги:
-
На ВМ посмотри, кто слушает и на каком адресе (разбор
ss -tlnpв уроке 2.1):sudo ss -tlnp -
На ВМ запусти временный сервер в фоне.
python3 -m http.server 9000раздаёт файлы текущей папки по HTTP на порту 9000,--bind 0.0.0.0значит «слушать на всех адресах»,&в конце запускает команду в фоне и возвращает тебе приглашение:python3 -m http.server 9000 --bind 0.0.0.0 & -
С ноутбука проверь порты. Здесь важно и время: замерь его на глаз или добавь перед
ncсловоtime:nc -zv -w 3 VM_IP 22 nc -zv -w 3 VM_IP 443 nc -zv -w 3 VM_IP 9000 nc -zv -w 3 VM_IP 9001 -
Открой порт 9000, проверь снова с ноутбука, затем убери правило и останови сервер (на ВМ).
kill %1останавливает задачу номер 1 из фона этого терминала:sudo ufw allow 9000/tcp # с ноутбука: nc -zv -w 3 VM_IP 9000 и nc -zv -w 3 VM_IP 9001 sudo ufw status numbered sudo ufw delete allow 9000/tcp kill %1 -
С ноутбука проверь «Заметки» и прямой порт приложения. У
curl:-sSтише без полосы прогресса, но с ошибками,-o /dev/nullвыбрасывает тело ответа,-w '%{http_code}\n'печатает код ответа,--max-time 5ограничивает ожидание,-kне проверяет сертификат (наш самоподписанный из урока 2.6):curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 -k https://VM_IP/healthz curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 http://VM_IP:8080/healthz
Что должно получиться (шаг 3, Linux):
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
Connection to 172.17.0.2 443 port [tcp/https] succeeded!
nc: connect to 172.17.0.2 port 9000 (tcp) timed out: Operation now in progress
nc: connect to 172.17.0.2 port 9001 (tcp) timed out: Operation now in progress
Шаг 4, после ufw allow 9000/tcp:
Connection to 172.17.0.2 9000 port [tcp/*] succeeded!
nc: connect to 172.17.0.2 port 9001 (tcp) timed out: Operation now in progress
Шаг 5:
200
curl: (28) Connection timed out after 5003 milliseconds
000
В ss -tlnp (шаг 1) тебе важны такие строки. Имена процессов nginx повторяются много раз, по числу рабочих процессов, у тебя их будет столько, сколько ядер:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=733,fd=3),("systemd",pid=1,fd=50))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=698,fd=5),...)
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=698,fd=6),...)
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=675,fd=3))
LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=733,fd=4),("systemd",pid=1,fd=51))
Как читать вывод:
succeeded!значит, что соединение установилось (пакеты дошли, файрвол пропустил, кто-то слушает).[tcp/ssh]это лишь подсказка, какой сервис обычно живёт на этом порту.timed out: ответа не было ровно три секунды (-w 3). Порты 9000 и 9001 закрыты файрволом, причём для файрвола неважно, слушает ли на 9000 Python.- После
allow 9000/tcpпорт 9000 отвечает, а 9001 по-прежнему молчит: правило открыло только один порт. 200это код HTTP ответа «Заметок» (урок 2.4). Он пришёл через nginx на порту 443.- Вторая команда
curlне получила ответ за 5 секунд:Connection timed out after 5003 milliseconds.-wнапечатал000(кода HTTP нет, ответа не было). Порт 8080 снаружи закрыт: и файрволом, и тем, что «Заметки» слушают только127.0.0.1. - В
ssсмотри на четвёртую колонку.0.0.0.0:22,0.0.0.0:80,0.0.0.0:443это «на всех адресах»,127.0.0.1:8080это «только loopback». А[::]:22это тот же порт 22 по IPv6. - Порт 22 держат сразу
sshdиsystemd: так работаетssh.socket(см. теорию).
Дополнительный опыт на пять минут: sudo ufw allow 9001/tcp, затем с ноутбука nc -zv -w 3 VM_IP 9001. На стенде он ответил мгновенно: failed: Connection refused (порт открыт, слушателя нет). Потом замени правило: sudo ufw delete allow 9001/tcp и sudo ufw reject 9001/tcp, и nc снова ответит Connection refused: reject неотличим по симптому от «нет слушателя». Убери за собой: sudo ufw delete reject 9001/tcp.
Объясни себе:
- Почему приложение на 127.0.0.1:8080 недоступно снаружи ещё до файрвола, и зачем тогда файрвол?
- Что ты увидел бы вместо таймаута, если бы порт 9001 был открыт в
ufw, но никто его не слушал? - Почему
ss -tlnpне показывает состояниеufw?
Типичные ошибки:
nc: connect to VM_IP port 443 (tcp) failed: Connection refused: правило открыто, но nginx не запущен. Проверьsystemctl status nginx.curl: (7) Failed to connect to VM_IP port 443 after 3 ms: Couldn't connect to server: мгновенный отказ значит «слушателя нет» (илиreject), а не молчаливый файрвол. (Так пишет curl 8.5 в Ubuntu 24.04. В Ubuntu 26.04 curl 8.18 пишетCould not connect to server, а словаConnection refusedвидны только с-v.)bash: kill: %1: no such job: сервер запущен в другой сессии (или уже остановлен). Останови его там черезCtrl+C.OSError: [Errno 98] Address already in useпри запускеhttp.server: порт 9000 уже занят предыдущим запуском. Останови его (kill %1) или возьми другой порт.
Нейросеть может предложить правила
ufwи настройкиsshd_config, но не знает, откуда ты подключён. Не применяй её правила по SSH, пока не проверишь их в запасной сессии: ошибка отрежет тебя от сервера.
Задание 3. Кто стучится в SSH и как это притормозить
Цель: прочитать журнал sshd и включить ограничение частоты подключений.
Предскажи: сколько неудачных входов ты увидишь на ВМ в домашней сети, и сколько на ВМ в облаке с публичным адресом через час после создания?
Ответ
В локальной ВМ обычно ноль. В облаке с публичным IP уже через несколько минут появляются десятки строк Invalid user ... и Failed password: сканеры обходят диапазоны адресов.
Шаги:
-
На ВМ посмотри последние события sshd. Разбор:
journalctl -u sshпечатает журнал юнитаssh(в Ubuntu он называется именно так),--since "1 hour ago"это только за последний час,--no-pagerвыводит всё сразу без постраничного просмотра,tail -n 15оставляет 15 последних строк. Вторая команда считает:grep -cсчитает подходящие строки, а\|в шаблоне значит «или»:sudo journalctl -u ssh --since "1 hour ago" --no-pager | tail -n 15 sudo journalctl -u ssh --no-pager | grep -c 'Failed password\|Invalid user' -
С ноутбука устрой заведомо неудачный вход под несуществующим именем (когда ssh спросит пароль, введи любой) и найди его в журнале на ВМ. Флаги:
PreferredAuthentications=passwordзаставляет клиента пробовать только пароль,PubkeyAuthentication=noотключает ключи:ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no nosuchuser@VM_IPsudo journalctl -u ssh --since "2 min ago" --no-pager | grep -i 'nosuchuser' -
На ВМ замени простое правило на
limit. Сначала удаляется старое разрешение, иначе оно стоит выше и пропускает всех, не считая частоту. Обрати внимание: между двумя командами нет правила для 22. Установленные сессии переживут это, но выполняй команды подряд:sudo ufw delete allow 22/tcp sudo ufw limit 22/tcp comment 'ssh limit' sudo ufw status numbered | grep 22 -
С ноутбука проверь ограничение серией подключений. Цикл
for i in 1 2 ...; do ...; doneповторяет команду для каждого значения,2>&1присоединяет сообщения об ошибках к обычному выводу,tail -n 1оставляет последнюю строку:for i in 1 2 3 4 5 6 7 8; do nc -zv -w 2 VM_IP 22 2>&1 | tail -n 1; done
Что должно получиться:
Шаг 2, на ВМ:
Sep 30 11:54:23 5a4d47f76e01 sshd[1142]: Invalid user nosuchuser from 172.17.0.13 port 54492
Sep 30 11:54:24 5a4d47f76e01 sshd[1142]: Failed password for invalid user nosuchuser from 172.17.0.13 port 54492 ssh2
Sep 30 11:54:26 5a4d47f76e01 sshd[1142]: Connection closed by invalid user nosuchuser 172.17.0.13 port 54492 [preauth]
Шаг 3:
Rule deleted
Rule deleted (v6)
Rule added
Rule added (v6)
[ 3] 22/tcp LIMIT IN Anywhere # ssh limit
[ 6] 22/tcp (v6) LIMIT IN Anywhere (v6) # ssh limit
Шаг 4:
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
Connection to 172.17.0.2 22 port [tcp/ssh] succeeded!
nc: connect to 172.17.0.2 port 22 (tcp) failed: Connection refused
nc: connect to 172.17.0.2 port 22 (tcp) failed: Connection refused
nc: connect to 172.17.0.2 port 22 (tcp) failed: Connection refused
Через 30 секунд после последней попытки доступ возвращается (succeeded!).
Как читать вывод:
- Три строки в журнале с одним PID (
1142) это один неудачный вход.Invalid userзначит, что такого пользователя на сервере нет.Failed password for invalid userзначит, что пароль не принят.[preauth]значит, что клиент так и не прошёл аутентификацию. Если ты ничего не вводил и оборвал вход черезCtrl+C,Failed passwordне будет, толькоInvalid userиConnection closed. - Число из второй команды шага 1 это все такие события за всё время работы журнала.
LIMIT INв списке: правилоlimitвместоALLOW. Номер[3]вместо[1]: удалённое правило освободило место, а новое добавилось в конец списка IPv4.- Первые пять подключений прошли, шестое и дальше получили
Connection refused. Это неожиданно, если ждёшь таймаут:limitработает черезREJECT(см. теорию). Порог: шесть новых подключений с одного адреса за 30 секунд.
Объясни себе:
- Что случится со скриптами деплоя из CI (урок 6.3), которые ходят по SSH часто, если включён
limit? - Почему строка
Invalid userне тревожит, аAccepted publickeyдля неизвестного пользователя тревожит? - Чем
limitотличается от fail2ban?
Типичные ошибки:
Could not delete non-existent rule(а для IPv6 ещё иCould not delete non-existent rule (v6)): правило записано иначе (напримерOpenSSHили ужеlimit). Смотриsudo ufw status numberedи удаляй по номеру (sudo ufw delete 1,ufwспроситProceed with operation (y|n)?, ответьy).-- No entries --уjournalctl -u ssh: на твоей системе юнит называется иначе. В Debian и Ubuntu онssh, в RHEL и Fedorasshd:sudo journalctl -u sshd.ssh: connect to host VM_IP port 22: Connection refusedпри быстром тестировании: ты сам попал подlimit. Подожди 30 секунд.
Задание 4. Шаг проекта: защита SSH в «Заметках»
Цель: зафиксировать в репозитории ~/notes настройки sshd, применить их на сервере и проверить эффективные значения. Вместе с заданием 1 это даёт итог: снаружи 22, 80, 443, порт 8080 закрыт, вход по ключам.
Предскажи: в /etc/ssh/sshd_config.d/ лежит 50-cloud-init.conf с PasswordAuthentication yes. Какое значение покажет sshd -T после добавления 99-notes.conf с no?
Ответ
yes: побеждает первое значение, а 50-... читается раньше 99-.... Поэтому проверяем результат через sshd -T, а конфликтующую строку правим.
Шаги:
-
Не закрывай текущую SSH-сессию. С ноутбука проверь, что вход по ключу работает:
ssh -o PasswordAuthentication=no USER@VM_IP true(trueэто команда, которая ничего не делает и завершается успешно, так проверяют сам вход). Если получаешьPermission denied (publickey), вернись к уроку 2.2 и настрой ключ, иначе ты сам себя запрёшь. -
На ВМ создай файл в репозитории проекта и установи его. Команда
cat > файл <<'CONF' ... CONFзаписывает в файл всё до строкиCONF(кавычки вокруг'CONF'отключают подстановки, урок 1.6).install -m 644 -o root -g rootкопирует файл и сразу задаёт права644и владельцаroot:root(sshd не должен читать конфиг, который может править кто угодно):mkdir -p ~/notes/deploy/ssh cat > ~/notes/deploy/ssh/99-notes.conf <<'CONF' # Настройки sshd для сервера «Заметок» PasswordAuthentication no PermitRootLogin no PubkeyAuthentication yes CONF sudo install -m 644 -o root -g root ~/notes/deploy/ssh/99-notes.conf /etc/ssh/sshd_config.d/99-notes.conf -
Проверь синтаксис и эффективные значения.
&&запускает вторую команду, только если первая завершилась успешно.grep -Ei '^(...|...) 'это расширенное регулярное выражение (-E) без учёта регистра (-i): строка должна начинаться (^) с одного из трёх слов (|значит «или») и дальше пробел:sudo sshd -t && echo "синтаксис ок" sudo sshd -T | grep -Ei '^(passwordauthentication|permitrootlogin|pubkeyauthentication) 'На стенде вторая команда показала:
синтаксис ок permitrootlogin no pubkeyauthentication yes passwordauthentication yesПароль всё ещё
yes: сработала ловушка из теории. -
Найди виновника, закомментируй его строку и проверь снова.
sed -i 's/^PasswordAuthentication yes/#PasswordAuthentication yes/'(правка файла на месте, урок 1.2): найти строку, которая начинается сPasswordAuthentication yes, и поставить перед ней#. Так строка становится комментарием, а файл остаётся на месте:grep -rn -i 'PasswordAuthentication' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ sudo sed -i 's/^PasswordAuthentication yes/#PasswordAuthentication yes/' /etc/ssh/sshd_config.d/50-cloud-init.conf sudo sshd -T | grep -i passwordauthentication/etc/ssh/sshd_config:66:#PasswordAuthentication yes /etc/ssh/sshd_config.d/50-cloud-init.conf:3:PasswordAuthentication yes /etc/ssh/sshd_config.d/99-notes.conf:2:PasswordAuthentication no passwordauthentication noФорма
sed -iздесь для GNU sed (Ubuntu). На macOS писали быsed -i '', но команда выполняется на сервере. Еслиgrepне показал вsshd_config.dничего, кроме твоего файла, аsshd -Tуже показываетno, значит, конфликта в твоей системе нет и шаг можно пропустить. (Например, на Ubuntu 24.04 из облачного образа встречается файл60-cloudimg-settings.confсPasswordAuthentication no: он ничему не мешает.) -
Перечитай конфигурацию и проверь новой сессией с ноутбука. Первую сессию не закрывай, пока вторая не заработает:
sudo systemctl reload sshssh -o PasswordAuthentication=no USER@VM_IP 'echo вход по ключу работает' ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password USER@VM_IP ssh -o PasswordAuthentication=no root@VM_IP -
Итоговая проверка: снаружи открыты 22, 80, 443, а приложение слушает только localhost. Затем зафиксируй файл в git (урок 1.1 и репозиторий
~/notes):sudo ufw status numbered sudo ss -tlnp | grep -E ':(22|80|443|8080) ' cd ~/notes && git add deploy/ssh/99-notes.conf && git commit -m "deploy: настройки sshd (ключи, без root)"
Что должно получиться:
синтаксис ок
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
вход по ключу работает
Вторая команда с ноутбука завершается ошибкой USER@VM_IP: Permission denied (publickey)., третья (под root) тоже Permission denied (publickey).. В выводе ss порты 22, 80, 443 слушают на 0.0.0.0, а 8080 только на 127.0.0.1. После коммита git печатает 1 file changed, 4 insertions(+) и create mode 100644 deploy/ssh/99-notes.conf. Состояние проекта: добавлен deploy/ssh/99-notes.conf, версия app.py прежняя (v3).
Как читать вывод:
синтаксис окнапечатала нашаecho:sshd -tсам молчит при успехе.- Порядок строк
sshd -Tне алфавитный, не удивляйся. Важны значения. На Ubuntu 24.04permitrootloginпосле правки равенno, а без правки было быwithout-password(см. теорию). Permission denied (publickey): сервер жив, но пароль не принимает (в скобках указаны допустимые способы: только ключ). Это желаемый результат для второй и третьей команд.- В
ss: 22, 80 и 443 на0.0.0.0доступны с любого адреса (файрвол их пропускает),127.0.0.1:8080это loopback. - Если у тебя на ВМ ещё не было первого подключения по SSH и
sudo sshd -TпишетMissing privilege separation directory: /run/sshd, это ssh.socket (см. теорию). Подключись по SSH один раз или выполниsudo mkdir -p /run/sshd.
Объясни себе:
- Почему порядок «проверил ключ, потом отключил пароль» обязателен, а не наоборот?
- Чем
systemctl reload sshбезопаснееrestartпри удалённой правке? - Почему порт 8080 защищён двумя слоями:
HOST=127.0.0.1вnotes.envи правилами файрвола?
Типичные ошибки:
/etc/ssh/sshd_config.d/99-notes.conf: line 2: Bad configuration option: PasswordAuthenticaton: опечатка в имени опции. Исправь и сноваsudo sshd -t. Не перечитывай sshd, пока-tне молчит (ниже этой строки будет ещёterminating, 1 bad configuration options).Missing privilege separation directory: /run/sshd: каталог создаёт запущенный sshd, а он ещё не запускался (сокетная активация) или остановлен.sudo mkdir -p /run/sshdи повториsshd -t.Permission denied (publickey).при входе новой сессией: в~/.ssh/authorized_keysнет твоего ключа или права на~/.sshшире700. Исправь из открытой первой сессии (урок 2.2).Failed to reload ssh.service: Unit ssh.service not found.: на этой системе юнит называется иначе (напримерsshdв RHEL и Fedora).
Сломай и почини
Скачай скрипт и запусти сценарий. Сам скрипт не читай: диагностика начинается с симптома. Скрипт меняет правила ufw и настройки sshd, поэтому запускай его только на учебной ВМ с консольным доступом (multipass shell, консоль облака). Сценарии 1 и 3 отрезают новые входы по SSH: уже открытая сессия остаётся, но лечить придётся через консоль. На всякий случай скрипт ставит таймер, который сам выполнит fix через 30 минут.
curl -fsSL -o /tmp/break-2.7.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/2.7/break.sh
sudo bash /tmp/break-2.7.sh 2
У curl флаг -f значит «при ошибке сервера не сохраняй страницу с ошибкой», -L разрешает переходы по перенаправлениям, -o задаёт имя файла.
Сценарии: 1, 2, 3. Проходи по одному: запусти, найди причину, почини сам или командой sudo bash /tmp/break-2.7.sh fix, потом бери следующий. В конце выполни fix и удали скрипт: rm /tmp/break-2.7.sh.
Симптом
- Сценарий 1. С ноутбука
ssh USER@VM_IPзаканчиваетсяssh: connect to host VM_IP port 22: Connection timed out. Ранее открытая сессия жива, а порты 80 и 443 отвечают. - Сценарий 2.
curl -sS --max-time 5 -k https://VM_IP/healthzзаканчиваетсяcurl: (28) Connection timed out after 5003 milliseconds. Аcurl -sS -o /dev/null -w '%{http_code}\n' http://VM_IP/отвечает301: порт 80 работает и перенаправляет на HTTPS. SSH тоже работает. - Сценарий 3.
ssh USER@VM_IPзаканчиваетсяUSER@VM_IP: Permission denied ()., хотя ключ раньше работал (скобки пустые, а не(publickey)).
Гипотезы
Для «порт не отвечает» назови три гипотезы: файрвол, сервис не слушает, не тот адрес. Для Permission denied тоже три: ключ не тот, неверные права на файлы, sshd не принимает ключи. Подумай, какую гипотезу подтверждает таймаут, а какую быстрый отказ (таблица «три ответа сети» из урока 2.1 и теория этого урока).
Проверки
Идём снизу вверх, на самом хосте (через консоль, если SSH недоступен). Помни, что проверять доступность порта с самого сервера бессмысленно, файрвол его не видит:
sudo ufw status numbered
sudo ss -tlnp | grep -E ':(22|80|443) '
sudo systemctl status nginx --no-pager
sudo sshd -T | grep -Ei '^(pubkeyauthentication|passwordauthentication) '
sudo journalctl -u ssh --since "10 min ago" --no-pager | tail -n 20
grep -rn -i 'PubkeyAuthentication' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
Различай: таймаут значит фильтр, мгновенный отказ значит «никто не слушает», Permission denied значит «сервер жив, аутентификация не прошла».
Исправление
Разбор сценариев
Сценарий 1. Файрвол включён, правила для 22 нет: ufw status numbered показывает только 80 и 443. ss при этом показывает, что sshd (и ssh.socket) слушает 22: слушатель есть, виноват фильтр. Лечение через консоль ВМ: sudo ufw allow 22/tcp. Урок: правило SSH до enable и отложенный откат systemd-run --on-active=5min ... ufw disable.
Сценарий 2. В списке правил нет 443. ss показывает, что nginx слушает 443, значит, виноват фильтр (порт слушают, а пакеты не доходят). Лечение: sudo ufw allow 443/tcp, затем curl -k https://VM_IP/healthz с ноутбука. Проверять с самой ВМ бесполезно: curl на её собственный адрес пойдёт через lo, и файрвол его пропустит.
Сценарий 3. sshd -T показывает pubkeyauthentication no: вход по ключу отключён, а пароли уже запрещены, значит, сервер не предлагает клиенту ни одного способа, и в скобках пусто. Журнал journalctl -u ssh показывает Connection closed by authenticating user USER ... [preauth]. grep -rn -i PubkeyAuthentication /etc/ssh/ находит файл 20-break-2.7.conf, который читается раньше 99-notes.conf. Лечение через консоль: удалить этот файл (или строку PubkeyAuthentication no), затем sudo sshd -t и sudo systemctl reload ssh. Урок: после правки проверять вход новой сессией, не закрывая старую, и проверять sshd -T.
Правило для дежурного:
Connection timed out: пакет пропал по пути. Сначала файрвол (ufw status numbered), потом облачный экран, потом маршрут.Connection refused: пакет дошёл, слушателя нет (или стоитreject). Ищи на сервере:ss -tlnp.Permission denied: сервер жив, аутентификация не прошла. Ищи вsshd -T, в журнале и в правах~/.ssh.
ИИ в помощь
Нейросеть хорошо объясняет правила файрвола и параметры SSH, но не видит, какое соединение у тебя открыто сейчас, и любит советовать «включить ufw и всё закрыть». Общие правила работы с ней: ИИ-помощник.
Задача: разобрать правила файрвола.
Вот вывод sudo ufw status numbered с моего сервера (адреса я заменил):
<вставь вывод>.
Объясни, что разрешено и что запрещено, в каком порядке срабатывают правила и что увидит клиент при запрещённом порту.
Скажи, чего в списке не хватает для сайта на портах 80 и 443 и SSH на 22.
Проверь ответ: сверь порядок правил по номерам: срабатывает первое подходящее. Типичная ошибка нейросети: путает deny и reject: deny молчит, а reject отвечает отказом.
Задача: найти причину, по которой настройка SSH не применилась.
Я положил PasswordAuthentication no в /etc/ssh/sshd_config.d/99-notes.conf, но пароль всё равно принимается.
Вот вывод sudo sshd -T | grep -i passwordauthentication: <вставь вывод>
и вывод grep -rn -i PasswordAuthentication /etc/ssh/sshd_config /etc/ssh/sshd_config.d/: <вставь вывод>.
Объясни причину и предложи безопасный порядок правки. Ничего не применяй сам.
Проверь ответ: убедись, что названо правило «побеждает первое значение» и порядок файлов. Типичная ошибка: предлагает restart без sshd -t и без второй открытой сессии.
Задача: проверить, что видно снаружи.
У меня на сервере запущены контейнеры и ufw. Вот вывод docker ps: <вставь вывод> и sudo ss -tlnp: <вставь вывод>.
Какие порты доступны снаружи, несмотря на ufw? Для каждого скажи, как закрыть и как проверить с другой машины.
Проверь ответ: проверь с другой машины через nc -zv. Типичная ошибка: забывает, что Docker обходит ufw, и считает порт закрытым по ufw status.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Файрвол (firewall) | Фильтр на входе в машину: решает по правилам, какие пакеты пропустить |
| Сетевой экран облака | Файрвол в панели облачного провайдера: стоит перед машиной, пакет до неё не доходит |
| Сканер | Бот, который перебирает адреса и порты в интернете в поисках открытого |
SSH-сервер (sshd) |
Программа на сервере, принимающая входящие SSH-подключения |
| Журнал SSH | Файл, куда сервер записывает попытки входа |
| netfilter | Подсистема ядра Linux, которая фильтрует пакеты |
| iptables, nftables | Инструменты записи правил в netfilter; nftables новее |
| ufw | Надстройка Ubuntu над iptables с короткими командами |
| Цепочка (chain) | Список правил для определённой точки на пути пакета: INPUT, FORWARD, OUTPUT |
| INPUT, FORWARD, OUTPUT | Пакеты «для этой машины», транзитные, исходящие от неё |
| Политика по умолчанию | Что делать с пакетом, к которому не подошло ни одно правило |
| allow, drop (deny), reject | Пропустить; молча выбросить; выбросить с ответом-отказом |
| Порт | Число от 0 до 65535, «номер квартиры» программы на машине |
| Слушать порт (listen), сокет | Ждать входящих соединений; точка подключения, которую программа открывает |
| TCP, SYN, RST | Надёжный протокол; первый пакет соединения; пакет-сброс («здесь никого нет») |
| Connection refused | Пришёл RST: хост жив, порт никто не слушает (или стоит reject) |
| Timed out | Ответа нет совсем: пакет потерян или выброшен файрволом |
| Stateful, conntrack, ESTABLISHED | Файрвол помнит соединения; пакеты уже открытых он пропускает |
(v6) в ufw |
Копия правила для IPv6; ufw создаёт правила для обеих версий |
limit в ufw |
Разрешить порт, но отклонять частые подключения (6 за 30 секунд с одного адреса) |
| Отложенный откат | Таймер systemd-run, который выключит файрвол, если ты себя запрёшь |
| DOCKER-USER, DNAT | Цепочка для своих правил перед правилами Docker; подмена адреса получателя, на которой держится публикация портов |
| sshd | Программа-сервер SSH |
sshd_config.d, Include |
Папка дополнительных файлов настроек sshd, читается по алфавиту |
sshd -t, sshd -T |
Проверить синтаксис конфигурации; показать итоговые значения опций |
| ssh.socket | Юнит, который слушает порт 22 и запускает sshd по первому подключению (Ubuntu 24.04+) |
reload |
Попросить сервис перечитать настройки, не останавливая его и не рвя сессии |
PasswordAuthentication, PermitRootLogin, PubkeyAuthentication |
Опции sshd: вход по паролю, вход под root, вход по ключу |
Invalid user, Failed password, Accepted publickey |
Строки журнала sshd: несуществующее имя, неверный пароль, успешный вход по ключу |
| fail2ban | Программа, которая банит адреса за неудачные входы |
| Бастион | Единственный сервер с открытым наружу SSH, через него ходят на остальные |
| cloud-init | Программа первого запуска облачной ВМ, создаёт часть конфигов, например 50-cloud-init.conf |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Сайт не открывается. Как понять, что виноват файрвол, а не приложение?
Ответ
Смотрю, как именно он не открывается. Таймаут говорит о фильтре: пакеты теряются. Мгновенный Connection refused значит, что хост жив, а на порту никто не слушает (ядро ответило RST). Проверяю nc -zv host 443 снаружи и ss -tlnp на самом сервере. Если на сервере порт слушает, а снаружи таймаут, иду смотреть ufw status и группы безопасности облака (правила доступа вне самой машины).
Что хотят услышать: различие timeout и refused, проверка с двух сторон (снаружи и на хосте), несколько слоёв фильтрации (хост, облако).
Красный флаг: «выключу файрвол и посмотрю» на боевом сервере.
2. [junior] [часто] [на скорость] Чем ufw отличается от iptables и nftables?
Ответ
iptables и nftables это интерфейсы к netfilter (фильтру пакетов в ядре), nft современнее. ufw это надстройка Ubuntu: короткий синтаксис, который сам генерирует эти правила. В современных системах iptables часто уже транслируется в nftables (iptables -V показывает nf_tables).
Что хотят услышать: ufw не отдельный файрвол, а оболочка; фильтрует одно ядро; умею посмотреть итог (iptables -S, nft list ruleset).
Красный флаг: «ufw и iptables это два разных файрвола, которые конфликтуют».
3. [junior] [часто] Как ты защищаешь SSH на новом сервере?
Ответ
Вход только по ключам ed25519 (PasswordAuthentication no), запрет root (PermitRootLogin no), работа под обычным пользователем с sudo. Порт 22 открыт файрволом, лучше только с нужных адресов или через бастион (единственный сервер с открытым SSH). Плюс ufw limit или fail2ban (программа, которая банит адреса за неудачные входы) против перебора и регулярные обновления безопасности. Итог проверяю через sudo sshd -T.
Что хотят услышать: три настройки sshd, проверка sshd -T, ограничение источников, порядок «сначала проверить ключ».
Красный флаг: «сменю порт на 2222, и этого достаточно».
4. [junior] Ты включил ufw enable по SSH и потерял доступ. Что произошло и как выбираться?
Ответ
Скорее всего, не было правила для 22, и новые подключения отбрасываются (текущая сессия жива, пока не оборвётся, потому что файрвол пропускает пакеты уже установленных соединений). Выбираюсь через консоль провайдера (serial или VNC: доступ к машине мимо сети): ufw allow 22/tcp. На будущее: сначала allow 22, потом enable, а на удалённом сервере ставлю отложенный ufw disable через systemd-run или at (утилита запуска команды в заданное время).
Что хотят услышать: консольный доступ как запасной вход, порядок правил, отложенный откат.
Красный флаг: «переустановлю сервер» или «зайду по SSH с другого адреса».
5. [middle] Ты закрыл порт 5432 в ufw, а сканер видит его открытым. Почему?
Ответ
Скорее всего, порт опубликован Docker: docker run -p 5432:5432 добавляет своё правило DNAT (подмену адреса получателя на адрес контейнера), после чего пакет считается транзитным и идёт через цепочку FORWARD, а правила ufw стоят в INPUT. Проверяю docker ps и sudo iptables -t nat -S DOCKER. Исправление: публиковать на localhost (127.0.0.1:5432:5432), не публиковать вообще (общая сеть Compose) или ограничить в цепочке DOCKER-USER (в ней Docker проверяет правила раньше своих).
Что хотят услышать: Docker обходит ufw, DOCKER-USER, публикация на localhost, принцип «базу наружу не выставляем».
Красный флаг: «ufw же показывает, что закрыто, значит, всё закрыто».
6. [middle] Прод после деплоя отвечает 502, а порты все открыты. Твои действия?
Ответ
Файрвол ни при чём: 502 значит, что nginx доехал, а апстрим (сервис, куда он передаёт запрос) не ответил. Смотрю error.log nginx, systemctl status notes, curl 127.0.0.1:8080/healthz на сервере. Если апстрим на другой машине, проверяю, не режет ли трафик файрвол между ними.
Что хотят услышать: иду по слоям, не по догадкам; код ответа как индикатор слоя; 502 это апстрим, 504 таймаут апстрима.
Красный флаг: сразу перезагружать сервер или nginx.
7. [middle] Ты отключил пароли, но ssh всё равно спрашивает пароль. Где смотреть?
Ответ
Проверяю итоговую конфигурацию sudo sshd -T | grep -i passwordauthentication. Частая причина: более ранний файл в sshd_config.d (например 50-cloud-init.conf) задаёт yes, а у sshd побеждает первое значение. Ещё возможен блок Match (условный блок в конфиге, который меняет опции для отдельных пользователей или адресов) или sshd, который не перечитал конфигурацию (не сделан reload).
Что хотят услышать: правило «первое значение побеждает», sshd -T, sshd -t, Include, reload.
Красный флаг: «перезагружу сервер, оно применится».
8. [middle] Как безопасно менять конфигурацию sshd на удалённом сервере?
Ответ
Не закрывая рабочую сессию: sshd -t, потом systemctl reload ssh, потом вторая сессия с проверкой входа. Только после этого закрываю первую. Reload не рвёт уже установленные сессии (sshd получает SIGHUP и перечитывает настройки). Для рискованных правок ставлю таймер отката или держу консоль провайдера.
Что хотят услышать: вторая сессия, -t, reload вместо restart, запасной канал.
Красный флаг: правка и restart в единственной сессии.
9. [middle] Разработчику нужен доступ к базе только с офисного адреса. Как это сделать?
Ответ
sudo ufw allow from 203.0.113.10 to any port 5432 proto tcp, затем проверяю ufw status numbered и подключение с его адреса. Порядок правил важен: запреты для этого порта должны стоять выше общего разрешения. Лучше вообще не выставлять базу наружу, а дать доступ через SSH-туннель (ssh -L: локальный порт, который пересылает данные на сервер через SSH; разобран в уроке 2.2, раздел про туннель -L) или VPN (защищённый канал в сеть). В облаке то же ограничение ставят в группе безопасности.
Что хотят услышать: правило с источником, порядок правил, предпочтение туннелю или VPN, документирование правила.
Красный флаг: allow 5432 для всех «на пять минут».
10. [junior] [на скорость] Порт закрыт файрволом или сервис не запущен? Как отличить по поведению?
Ответ
Если пакеты роняет файрвол (deny), клиент ждёт таймаута. Если хост отвечает, но никто не слушает, ядро шлёт RST и клиент сразу получает Connection refused. Подтверждаю: ss -tlnp на сервере и nc -zv снаружи. Правило reject (и ufw limit при превышении лимита) тоже даёт мгновенный отказ, похожий на «не слушают», это оговорка: смотрю ufw status numbered на сервере.
Что хотят услышать: timeout против refused, проверка с обеих сторон, оговорка про reject.
Красный флаг: «оба случая выглядят одинаково».
11. [middle] В логах SSH тысячи неудачных входов. Это инцидент?
Ответ
Не обязательно: обычно это фоновый шум сканеров. Проверяю, что пароли отключены и root запрещён (тогда подбирать нечего), ищу Accepted в журнале: нет ли неизвестных пользователей или адресов, смотрю last (список последних входов) и authorized_keys. Шум снижаю ufw limit или fail2ban. Инцидент это успешный вход неизвестного источника или изменение authorized_keys.
Что хотят услышать: различие шума и компрометации, поиск Accepted, last, контроль authorized_keys.
Красный флаг: «сразу переустановлю сервер» или «игнорирую, это всегда шум».
12. [middle] Диск сервера растёт из-за логов файрвола и журнала. Что делаешь?
Ответ
Смотрю, что пишет: sudo ufw logging low вместо high (уровень подробности журнала блокировок), проверяю ротацию (logrotate: программа, которая архивирует и удаляет старые логи) и лимит журнала (journalctl --disk-usage, параметр SystemMaxUse в journald.conf). Логи блокировок полезны при расследовании, но шум ботов на 22 их раздувает. ufw limit и уровень low обычно достаточны.
Что хотят услышать: уровни логирования, ротация, лимит журнала, баланс между шумом и расследованием.
Красный флаг: «удалю логи» без настройки ротации.
13. [middle] Что такое stateful-файрвол и как его правила учитывают состояние соединений?
Ответ
Stateful-файрвол помнит состояние соединений (connection tracking) и решает по нему. Типичная схема: разрешить новые входящие соединения только на нужные порты, а пакеты со статусом ESTABLISHED,RELATED пропускать автоматически. Поэтому ответы на мои исходящие запросы возвращаются без отдельного правила. В ufw это встроено, в iptables пишут -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT. Таблицу соединений смотрю через conntrack -L. Если она переполняется, новые соединения начинают отбрасываться, а в журнале ядра появляется nf_conntrack: table full.
Что хотят услышать: соединения отслеживаются, ESTABLISHED,RELATED, ответы разрешены автоматически, conntrack, переполнение таблицы.
Красный флаг: «Для каждого ответа пишу отдельное правило».
14. [junior] [на скорость] Что такое политика по умолчанию «запретить всё» (default deny) и зачем она?
Ответ
При default deny всё входящее блокируется, а разрешается только то, что явно нужно: SSH, 80, 443. Так новый сервис, который случайно слушает порт на всех интерфейсах, автоматически не оказывается в интернете. В ufw это ufw default deny incoming и ufw default allow outgoing. Порядок действий на удалённом сервере: сначала добавить разрешение на SSH, потом включать файрвол, чтобы не потерять доступ. Белый список проще проверять, чем чёрный: достаточно перечитать список разрешённого.
Что хотят услышать: разрешено только нужное, ufw default deny incoming, сначала SSH потом enable, безопаснее чёрного списка.
Красный флаг: «Открываю всё, а потом закрываю то, что заметил».
15. [middle] Чем security group в облаке отличается от ufw на самом сервере?
Ответ
Security group работает на уровне облачной сети, снаружи виртуальной машины: трафик блокируется до попадания на хост, правила управляются через консоль или Terraform. ufw работает внутри ОС и защищает хост независимо от облака. Обычно security group stateful и по умолчанию запрещает входящее. Для диагностики важно помнить об обоих слоях: порт может быть открыт в ufw, но закрыт в группе, и наоборот. Поэтому проверяю по порядку: слушает ли процесс (ss -tlnp), что разрешено в ufw status, что разрешено в security group. Оба слоя лучше держать согласованными.
Что хотят услышать: слой сети и слой хоста, правила в Terraform или консоли, два слоя нужно проверять, ss -tlnp и ufw status.
Красный флаг: «Достаточно открыть порт в одном месте, остальное неважно».
Проверено на версиях
Практика прогнана на стенде: Docker, образ Ubuntu 24.04.5 LTS с systemd 255 (ядро хоста 6.12.76-linuxkit), два контейнера: «ВМ» и «ноутбук» в одной сети Docker.
- ufw 0.36.2, iptables v1.8.10 (nf_tables): задания 1-3, порядок правил,
limit,reject, скрипт поломок. - OpenSSH 9.6p1 (
sshd -t,sshd -T,reload, вход по ключу, отказы, журнал), задание 4. - nginx 1.24.0 и «Заметки»
app.pyv3 (Python 3.12.3) за ним, порты 22, 80, 443, 8080; curl 8.5.0, netcat-openbsd 1.226, git 2.43.0. - Вывод
iptables -t nat -S DOCKERснят на Docker Desktop (Mac) с контейнеромpython:3.12-alpine, опубликованным как-p 5432:8000. ПравилаDOCKER-USERи запрет на публикацию порта на самом Linux-сервере сufwне прогонялись. - Ubuntu 26.04: установлены ufw 0.36.2, OpenSSH 10.2p1, nginx 1.28.3 и проверен вывод
sshd -T(permitrootlogin prohibit-password, без ошибки про/run/sshd). Остальная практика на 26.04 (systemd,ufw enable) не прогонялась. - macOS: тексты
ncдля таймаута и отказа проверены на macOS 27.0.1, успешное соединение и выводncпри Windows не проверялись. - Файл
50-cloud-init.confна стенде создан вручную, чтобы воспроизвести типичную ловушку. Точное имя такого файла зависит от облака и образа. - Скрипт
project/notes/break/2.7/break.sh:shellcheckбез замечаний, сценарии 1, 2, 3 иfix(повторный запуск тоже) прогнаны на стенде.
Итог урока: ты умеешь
- включить
ufwс политикойdeny incomingи правилами для 22, 80, 443, не потеряв доступ - поставить отложенный откат файрвола через
systemd-run - отличить закрытый файрволом порт (таймаут) от порта без слушателя (
Connection refused), и помнить проreject - проверить открытые порты снаружи через
nc -zvи изнутри черезss -tlnp - настроить sshd без паролей и без root и проверить итог через
sshd -T - безопасно применить конфигурацию sshd:
sshd -t,reload, вторая сессия - объяснить, почему Docker обходит
ufw, и как этого избежать - читать журнал
journalctl -u sshи ограничивать перебор черезufw limit
Дальше: Урок 2.8: Путь запроса и диагностика «сайт не открывается»
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.