Тема 11

Сеть из командной строки и SSH

Сеть из командной строки и SSH

Предыдущие темы не выходили за пределы одной машины: тема 4 показала пользователей и права доступа, тема 5 — конвейеры, тема 9 — службы и журналы, тема 10 — диски и монтирование файловых систем. Теперь терминал выходит за пределы компьютера. Сетевые команды показывают, какие адреса есть у машины, куда уходят пакеты и какие службы принимают соединения, а команды curl, dig и nc позволяют проверить работу удалённой службы без браузера. Протокол SSH (англ. Secure Shell) даёт возможность работать с удалённым компьютером так же, как с локальным, — через командную строку и по зашифрованному каналу. Именно SSH позволяет администрировать серверы, которые физически находятся в другом помещении или в другом городе 1; общие приёмы администрирования описаны в руководстве 2.

Тема намеренно не углубляется в устройство сети. Что такое сетевой уровень, как устроен IP-пакет, как работает TCP и почему DNS построена как иерархия, объясняется в курсе «Компьютерные сети и сетевые технологии». Здесь сетевые понятия вводятся только в том объёме, который нужен для работы в терминале: адрес, порт, имя. Если эти понятия уже знакомы по курсу сетей, раздел о понятиях можно пролистать.

Изложение идёт от словаря к командам, от проверки служб к SSH. Все команды выполнены в среде Ubuntu 22.04 под WSL2; целевая среда курса — Ubuntu 24.04, отличия для неё (запуск sshd через ssh.socket) оговорены в разделе о безопасности sshd. Адреса в выводе заменены на документационные из диапазона 192.0.2.0/24, структура вывода сохранена. Команды, требующие прав администратора, помечены как пример вывода, потому что в среде для их выполнения нет пароля sudo.

Сетевые понятия, которые нужны в работе

IP-адрес и сетевой интерфейс

Сетевой интерфейс (англ. network interface) — точка подключения компьютера к сети. Интерфейсом считается физический сетевой адаптер, виртуальный адаптер WSL2 и петлевой интерфейс lo, который существует в любой системе. Каждому интерфейсу может быть назначен IP-адрес (англ. IP address) — числовой идентификатор узла в сети. Устройство IPv4-адреса и маски подсети подробно разобрано в теме 6 курса «Компьютерные сети и сетевые технологии». В командной строке важно другое: адрес почти всегда записывается вместе с длиной префикса, например 192.0.2.10/24.

Число после косой черты задаёт, сколько первых битов адреса относятся к сети: для 192.0.2.10/24 сеть имеет вид 192.0.2.0/24, а 10 — номер узла в ней. Машины одной сети отправляют пакеты друг другу напрямую, всё остальное уходит через шлюз (см. раздел про маршрут).

Адрес 127.0.0.1 принадлежит петлевому интерфейсу lo. Пакет, отправленный на этот адрес, не покидает компьютер и возвращается его же программам. Именно поэтому примеры темы можно выполнять без подключения к внешней сети: клиент и сервер работают на одной машине. Имя localhost обычно соответствует этому адресу, но может разрешаться и в IPv6-адрес ::1.

Порт и служба

Один IP-адрес обслуживает одновременно много программ: веб-сервер, почтовый сервер, службу SSH. Чтобы входящий пакет попал к нужной программе, он адресуется не только на IP-адрес, но и на номер порта (англ. port). Порт — число от 0 до 65535. По соглашению порт 22 занимает sshd, порт 80 — HTTP-сервер, порт 443 — HTTPS. Учебные службы удобно запускать на портах выше 1024, например на порту 8080, потому что для таких портов не нужны права администратора.

Пара «адрес и порт» записывается через двоеточие: 192.0.2.10:8080. Служба, которая принимает соединения на порту, говорит, что она слушает (англ. listen) этот порт. Если на порту никто не слушает, клиент получает немедленный отказ, как показано на рисунке для порта 443. Отказ означает, что компьютер доступен, но службы на нужном порту нет. Это отличается от ситуации, когда компьютер не отвечает совсем. Механика сокетов и TCP рассматривается в теме 12 курса «Компьютерные сети и сетевые технологии».

Клиент, DNS и сервер: имя переводится в адрес, адрес и порт выбирают службу на сервере
Имя, адрес и порт: DNS переводит имя в IP-адрес, порт выбирает службу на сервере

Как показано на рисунке, программа на клиенте сначала получает у DNS адрес по имени, а затем обращается к адресу и порту. Порт 8080 обслуживает учебный веб-сервер, порт 22 — sshd, порт 443 в этом примере никем не занят.

Имя и DNS

Людям удобнее запоминать имена, например example.com, а сети работают с адресами. Система доменных имён (англ. Domain Name System, DNS) переводит имя в адрес. Клиент задаёт DNS-серверу вопрос «какой адрес у имени», и тот отвечает записью. Для имени типа A ответом служит IPv4-адрес. Иерархическое устройство DNS, зоны и делегирование рассматриваются в теме 15 курса «Компьютерные сети и сетевые технологии».

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

Сетевые команды: адреса, маршруты и порты

Для просмотра сетевых параметров в Linux используется команда ip из пакета iproute2. Она заменила устаревшие ifconfig и route, которые ещё встречаются в старых инструкциях. Команды этого раздела перечислены в таблице. Каждая из них изучается по выводу, а не заучивается наизусть.

КомандаЧто показываетДля чего нужна
ip -br addrинтерфейсы, их состояние и адресапроверить, назначен ли машине адрес
ip routeтаблицу маршрутовузнать, куда уходят пакеты и есть ли маршрут по умолчанию
ping -c 3 адресответы на служебные запросы ICMPпроверить доступность узла и время отклика
ss -tulpnслушающие порты и программы, которые их открылиувидеть службы, принимающие соединения
dig +short имяответ DNS-сервера в краткой формепроверить разрешение имени

Протокол ICMP, на котором работает ping, описан в теме 7 курса «Компьютерные сети и сетевые технологии». Для работы в терминале достаточно знать, что ping отвечает только на вопрос «доходит ли служебный запрос до узла».

Интерфейсы и адреса: ip -br addr

Краткий вывод команды ip выглядит так:

ip -br addr
lo               UNKNOWN        127.0.0.1/8 192.0.2.53/32 ::1/128
eth0             UP             192.0.2.10/24 fe80::215:5dff:fe5b:8b43/64
gre0@NONE        DOWN
gretap0@NONE     DOWN
erspan0@NONE     DOWN

Вывод содержит четыре колонки: имя интерфейса, состояние, адреса. Состояние UP означает, что интерфейс включён и имеет физическую или виртуальную связь с сетью. Интерфейс lo имеет состояние UNKNOWN, и это нормально: у петлевого интерфейса нет настоящей линии связи, и ядро не может сообщить иное состояние. Интерфейсы gre0, gretap0 и erspan0 — туннельные интерфейсы, которые ядро создаёт по умолчанию. Они находятся в состоянии DOWN и не используются в обычной работе.

Главный интерфейс машины в этом выводе — eth0. Его адрес 192.0.2.10/24 записан вместе с длиной префикса. Строка fe80::… относится к IPv6 и для изучения IPv4 не важна.

Подробный вывод по одному интерфейсу показывает команда с опцией -4, которая ограничивает вывод адресами IPv4:

ip -4 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    inet 192.0.2.10/24 brd 192.0.2.255 scope global eth0
       valid_lft forever preferred_lft forever

В первой строке флаги UP и LOWER_UP означают, что интерфейс включён и физический уровень работает. Параметр mtu 1500 задаёт наибольший размер пакета, который интерфейс передаёт без дробления. Во второй строке inet — IPv4-адрес, brd — широковещательный адрес сети (последний адрес сети 192.0.2.0/24), scope global означает, что адрес пригоден для связи за пределами машины. Строки valid_lft и preferred_lft задают время жизни адреса; у статического адреса оно бесконечно.

Маршрут: ip route

Таблица маршрутов отвечает на вопрос, через какой интерфейс и каким следующим узлом отправить пакет. Вывод команды ip route выглядит так:

ip route
default via 192.0.2.1 dev eth0 proto kernel
192.0.2.0/24 dev eth0 proto kernel scope link src 192.0.2.10

Первая строка — маршрут по умолчанию (англ. default route). Он применяется к любому адресу, для которого нет более точного маршрута. Слово via указывает шлюз, то есть маршрутизатор, который передаст пакет дальше: в данном случае 192.0.2.1. Вторая строка описывает собственную сеть машины. Признак scope link означает, что узлы этой сети доступны напрямую, без шлюза. Слово src указывает адрес, с которого машина отправляет пакеты в эту сеть.

Если строка с default отсутствует, машина не знает, куда отправить пакеты во внешнюю сеть: локальные адреса при этом работают, а внешние — нет.

Проверка связи: ping -c

Команда ping отправляет служебные запросы ICMP и измеряет время ответа. Опция -c задаёт число запросов. Без неё ping работает бесконечно, пока его не остановит комбинация Ctrl+C.

ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.022 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.022 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.035 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2048ms
rtt min/avg/max/mdev = 0.022/0.026/0.035/0.006 ms

Число 56(84) в первой строке означает, что полезные данные занимают 56 байт, а с учётом заголовков ICMP и IP пакет имеет размер 84 байта. Поле icmp_seq нумерует запросы, поле ttl (англ. time to live) ограничивает число переходов пакета; начальное значение 64 — типичный выбор Linux. В строке статистики видны число потерь и время отклика. Для 127.0.0.1 такие значения ожидаемы: пакет никуда не уходит.

Важно не делать ложных выводов. Если ping не получает ответа, это не значит, что узел выключен: администратор может запретить ICMP на файрволе, и служба при этом будет работать. Поэтому доступность службы проверяют не ping, а команды, которые обращаются к нужному порту, — curl и nc, о которых пойдёт речь ниже.

Слушающие порты: ss -tulpn

Команда ss (англ. socket statistics) показывает сокеты машины. Опции -t и -u выбирают протоколы TCP и UDP, -l оставляет только слушающие сокеты, -p добавляет имя программы, а -n выводит числовые адреса и порты вместо имён служб. Сочетание -tulpn — самая распространённая форма запроса.

На машине уже запущен учебный веб-сервер на порту 8080 (см. раздел про HTTP). Вывод команды:

ss -tulpn
Netid State  Recv-Q Send-Q  Local Address:Port Peer Address:PortProcess
udp   UNCONN 0      0      192.0.2.53:53        0.0.0.0:*
udp   UNCONN 0      0           127.0.0.1:323       0.0.0.0:*
udp   UNCONN 0      0               [::1]:323          [::]:*
tcp   LISTEN 0      5           127.0.0.1:8080      0.0.0.0:*    users:(("python3",pid=17174,fd=3))
tcp   LISTEN 0      1000   192.0.2.53:53        0.0.0.0:*

Колонка State показывает состояние сокета: LISTEN для TCP и UNCONN для UDP, то есть «слушает без установленного соединения». Колонка Local Address:Port задаёт адрес и порт, на которых служба принимает соединения. Адрес 127.0.0.1:8080 означает, что порт 8080 открыт только для программ на этой же машине. Если бы программа слушала адрес 0.0.0.0:8080, порт принимал бы соединения со всех интерфейсов, включая внешние. Адрес [::1] — то же самое, что 127.0.0.1, только для IPv6. Заголовок Peer Address:PortProcess склеен в выводе из двух полей, и это не ошибка.

Колонка users: показывает программу-владельца сокета (python3, pid=17174); процессы разобраны в теме 7. Без привилегий администратора ss не показывает программы других пользователей, поэтому нужен sudo. Пароля sudo в среде нет, и ниже приведён пример вывода:

sudo ss -tulpn | grep sshd
tcp   LISTEN 0      128          0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=812,fd=3))
tcp   LISTEN 0      128             [::]:22           [::]:*    users:(("sshd",pid=812,fd=4))

На машине с запущенным sshd служба слушает порт 22 на всех адресах. Это конвейер из темы 5: одна команда описывает сокеты, вторая выбирает нужные строки.

HTTP и DNS из командной строки

Веб-страницы удобно запрашивать программой curl. Протокол HTTP разобран в теме 16 курса «Компьютерные сети и сетевые технологии»; здесь важны формат запроса и поля ответа. Основные опции приведены в таблице.

ОпцияНазначение
-vподробный режим: показывает заголовки запроса (строки с >) и ответа (строки с <)
-Iотправляет запрос HEAD: выводит только заголовки ответа, тело не загружается
-o файлсохраняет тело ответа в файл вместо вывода на экран
-Lследует перенаправлениям, которые сервер сообщает кодами 3xx
-sскрывает индикатор прогресса (удобно в скриптах)
-w форматвыводит после запроса заданные сведения, например код ответа

curl: заголовки, подробный режим, сохранение, перенаправления

Простейший запрос выводит заголовки ответа и затем тело. Опция -I ограничивает вывод заголовками. Для учебной страницы она выглядит так:

curl -sI http://127.0.0.1:8080/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.10.12
Date: Mon, 12 Oct 2026 10:30:25 GMT
Content-type: text/html
Content-Length: 83
Last-Modified: Mon, 12 Oct 2026 10:15:31 GMT

Первая строка содержит версию протокола и код состояния 200 OK. Поле Content-Length сообщает размер тела в байтах, пустая строка отделяет заголовки от тела. Опция -s подавляет индикатор прогресса.

Для диагностики чаще нужна опция -v. Она показывает весь обмен: адрес, к которому подключилась программа, заголовки отправленного запроса и заголовки ответа. Строки запроса начинаются с символа >, строки ответа — с <:

curl -sv -o /dev/null http://127.0.0.1:8080/index.html
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /index.html HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.10.12
< Date: Mon, 12 Oct 2026 10:30:25 GMT
< Content-type: text/html
< Content-Length: 83
< Last-Modified: Mon, 12 Oct 2026 10:15:31 GMT
<
* Closing connection 0

Строки со звёздочкой описывают работу самого curl. Строка Host: содержит имя сайта, по которому сервер выбирает ресурс; здесь это адрес, так как обращение шло по IP. По строкам > и < видно, достигла ли программа сервера и что он ответил.

Для сохранения ответа в файл используется опция -o. Сохраним страницу и проверим размер файла:

curl -s -o page.html http://127.0.0.1:8080/index.html && ls -l page.html && cat page.html
-rw-r--r-- 1 student student 83 Oct 12 13:30 page.html
<!doctype html>
<html><body><h1>Учебная страница</h1></body></html>

Размер файла совпадает с Content-Length: тело сохранено без изменений.

Сервер может сообщить, что ресурс переехал. Такой ответ имеет код из класса 3xx и поле Location. Учебный сервер Python отвечает кодом 301, если в адресе каталога пропущен завершающий слеш:

curl -sI http://127.0.0.1:8080/docs
HTTP/1.0 301 Moved Permanently
Server: SimpleHTTP/0.6 Python/3.10.12
Date: Mon, 12 Oct 2026 10:30:25 GMT
Location: /docs/
Content-Length: 0

По умолчанию curl не следует перенаправлению. Опция -L включает переход по Location, -w выводит итоговый код и адрес:

curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' http://127.0.0.1:8080/docs
200 http://127.0.0.1:8080/docs/

Итоговый код 200 и адрес со слешем на конце показывают, что curl перешёл по перенаправлению. Запрос к несуществующему ресурсу даёт код 404:

curl -sI http://127.0.0.1:8080/nothere | head -1
HTTP/1.0 404 File not found

Здесь использован конвейер | head -1: из всего ответа выводится только первая строка. Такой приём удобен, когда нужен только код состояния.

Тестовый веб-сервер и его журнал

Для экспериментов достаточно модуля http.server из стандартной библиотеки Python 3, который раздаёт файлы каталога. Запустим его на порту 8080:

mkdir -p ~/lab-11/www/docs && cd ~/lab-11/www && echo '<h1>Учебная страница</h1>' > index.html && python3 -m http.server 8080 --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 8080 (http://127.0.0.1:8080/) ...

Сервер занимает терминал. Опция --bind 127.0.0.1 ограничивает прослушивание петлевым адресом; без неё страницы доступны всем адресам машины, включая локальную сеть. Остановить сервер можно комбинацией Ctrl+C.

Каждый запрос сервер записывает в журнал. Ниже приведены строки журнала, появившиеся после предыдущих команд curl:

127.0.0.1 - - [12/Oct/2026 13:15:33] "HEAD / HTTP/1.1" 200 -
127.0.0.1 - - [12/Oct/2026 13:15:33] "GET /index.html HTTP/1.1" 200 -
127.0.0.1 - - [12/Oct/2026 13:15:33] "HEAD /docs HTTP/1.1" 301 -
127.0.0.1 - - [12/Oct/2026 13:15:33] "GET /docs/ HTTP/1.0" 200 -
127.0.0.1 - - [12/Oct/2026 13:15:33] "HEAD /nothere HTTP/1.1" 404 -

Каждая строка содержит адрес клиента, время, метод, путь, код ответа и размер тела. Если запроса в журнале нет, проблема лежит раньше: в сети или в имени.

DNS: dig и host

Команда dig (англ. domain information groper) отправляет DNS-запрос напрямую и показывает ответ. Краткая форма с опцией +short выводит только адреса:

dig +short example.com
8.47.69.5
8.6.112.5

Адреса здесь настоящие (домен example.com) и могут измениться. Полный ответ содержит дополнительные поля:

dig example.com A +noall +answer
example.com.		140	IN	A	8.47.69.5
example.com.		140	IN	A	8.6.112.5

Поля записи: имя, время жизни в секундах (TTL, англ. time to live), класс IN (Internet), тип A (адрес IPv4) и значение.

nc: проверка порта

Команда nc (netcat) открывает соединение с адресом и портом. Опция -z не передаёт данных, -v выводит подробности: так проверяется, слушает ли порт кто-то.

nc -vz 127.0.0.1 8080
nc -vz 127.0.0.1 8081
Connection to 127.0.0.1 8080 port [tcp/http-alt] succeeded!
nc: connect to 127.0.0.1 port 8081 (tcp) failed: Connection refused

Строка succeeded означает, что служба на порту существует. Строка Connection refused означает, что адрес доступен, но порт никем не слушается. Название http-alt — стандартное имя порта 8080 из системной базы служб.

SSH: вход на удалённый компьютер

Модель клиент-сервер и первое подключение

SSH построен по модели клиент-сервер. Серверная часть — демон sshd на порту 22, клиентская — программа ssh. Команда ssh устанавливает зашифрованный канал и открывает оболочку на удалённой машине или выполняет одну команду. Версия клиента:

ssh -V
OpenSSH_8.9p1 Ubuntu-3ubuntu0.17, OpenSSL 3.0.2 15 Mar 2022

Клиент и сервер принадлежат проекту OpenSSH, справочник которого указан в списке литературы 5. Классическое руководство по SSH даёт полное описание протокола и его настройки 4.

Типичная команда входа имеет вид ssh пользователь@адрес. Например, ssh student@192.0.2.10 означает вход под учётной записью student на машине с адресом 192.0.2.10. При первом подключении клиент сообщает, что не знает ключ сервера, и предлагает его принять. Ниже приведён пример вывода, так как сервера в среде нет:

The authenticity of host '192.0.2.10 (192.0.2.10)' can't be established.
ED25519 key fingerprint is SHA256:xK3pV9… (сокращено).
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.0.2.10' (ED25519) to the list of known hosts.
student@192.0.2.10's password:

Клиент показывает отпечаток (англ. fingerprint) ключа сервера — короткую хэш-сумму. Сверка отпечатка с тем, что администратор видит на сервере, защищает от подмены посредником. Принятие ключа без сверки — распространённая ошибка новичков.

Файл known_hosts

Принятый ключ записывается в файл ~/.ssh/known_hosts. При следующих подключениях клиент сверяет ключ сервера с записью в этом файле. Совпадение означает, что сервер тот же. Если ключ изменился, клиент прерывает соединение и выводит предупреждение. Пример вывода:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Host key verification failed.

Предупреждение не всегда означает атаку: сервер мог быть переустановлен. Запись не следует удалять, не выяснив причину: сначала сверяется отпечаток с администратором, и только потом выполняется ssh-keygen -R 192.0.2.10, которая удаляет записи для узла.

Ключи: генерация, authorized_keys и ssh-copy-id

Альтернатива паролю — аутентификация по ключу. Закрытый ключ хранится только на клиенте, открытый передаётся серверу. Клиент доказывает владение закрытым ключом подписью, а сервер проверяет её открытым ключом. Схема показана на рисунке.

Схема аутентификации по ключу: клиент передаёт открытый ключ, сервер присылает данные для подписи, клиент подписывает их закрытым ключом, сервер проверяет подпись открытым ключом
Аутентификация по ключу: закрытый ключ не покидает клиент, сервер хранит только открытые ключи

Как показано на рисунке, пароль по сети не передаётся. Закрытый ключ нужно беречь как пароль, открытый можно показывать кому угодно.

Современный тип ключа — ed25519 на эллиптических кривых. Пример выполнен во временном каталоге /tmp/keys-demo, чтобы не трогать настоящие ключи. В реальной работе ключ создаётся в ~/.ssh и защищается парольной фразой; пустая фраза (-N "") здесь только для демонстрации:

mkdir -m 700 /tmp/keys-demo && ssh-keygen -t ed25519 -f /tmp/keys-demo/id_ed25519 -N "" -C "student@lab"
Generating public/private ed25519 key pair.
Your identification has been saved in /tmp/keys-demo/id_ed25519
Your public key has been saved in /tmp/keys-demo/id_ed25519.pub
The key fingerprint is:
SHA256:g5V5rEe61Iakz6Rk0cbScSVf/6v9I8oT9dKDi6kZ/5M student@lab
The key's randomart image is:
+--[ED25519 256]--+
|        . o.. .  |
|       + * o . . |
|      o X + .   .|
|       O B   .  .|
|      = S + . + .|
|     o * = . o +.|
|      . +.  + +..|
|          =+ Eo. |
|         o.++oo.+|
+----[SHA256]-----+

Опция -t задаёт тип ключа, -f — имя файла, -N — парольную фразу, -C — комментарий. Рисунок randomart — визуальная копия отпечатка.

Два файла созданы с разными правами. Закрытый ключ доступен только владельцу, открытый — для чтения всем:

ls -l /tmp/keys-demo
total 8
-rw------- 1 student student 399 Oct 12 13:16 id_ed25519
-rw-r--r-- 1 student student  93 Oct 12 13:16 id_ed25519.pub

Отпечаток открытого ключа показывает команда ssh-keygen -lf, а сам открытый ключ — обычный cat:

ssh-keygen -lf /tmp/keys-demo/id_ed25519.pub && cat /tmp/keys-demo/id_ed25519.pub
256 SHA256:g5V5rEe61Iakz6Rk0cbScSVf/6v9I8oT9dKDi6kZ/5M student@lab (ED25519)
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID4P9wLa4Q14q1Rev0i3UreYguLQKuWRXRZwUKb9hJ5h student@lab

Именно эта строка попадает на сервер в файл ~/.ssh/authorized_keys, по одной строке на разрешённый ключ: тип, ключ в base64 и комментарий.

Команда ssh-copy-id подключается к серверу, спрашивает пароль один раз и добавляет ключ в authorized_keys, не дублируя строки. Пример вывода, так как сервера в среде нет:

ssh-copy-id -i ~/.ssh/id_ed25519.pub student@192.0.2.10
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/student/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
student@192.0.2.10's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'student@192.0.2.10'"
and check to make sure that only the key(s) you wanted were added.

Сообщение Number of key(s) added: 1 означает, что ключ записан. Вход по ключу нужно проверить до отключения пароля.

Права доступа и диагностика ssh -v

Права на ключи и каталог ~/.ssh проверяются строго; чтение прав rwx описано в теме 4. Рекомендуемые значения:

ОбъектПраваПочему
каталог ~/.ssh700 (drwx------)в каталоге лежат ключи, поэтому доступ только владельцу
закрытый ключ id_ed25519600 (-rw-------)закрытый ключ нельзя читать другим пользователям
открытый ключ id_ed25519.pub644 (-rw-r--r--)открытый ключ не секрет, его можно читать
~/.ssh/authorized_keys на сервере600 (-rw-------)sshd принимает только файл, который не могут изменить посторонние
~/.ssh/config600 или 644файл настроек не должен быть доступен для записи другим пользователям

Клиент проверяет права закрытого ключа и отказывается его использовать, если он доступен другим. Демонстрация на копии ключа с правами 644:

cp /tmp/keys-demo/id_ed25519 /tmp/keys-demo/bad_key && chmod 644 /tmp/keys-demo/bad_key && ssh-keygen -y -f /tmp/keys-demo/bad_key
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/tmp/keys-demo/bad_key' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/tmp/keys-demo/bad_key": bad permissions

Строка This private key will be ignored объясняет, почему ключ не используется.

Сервер проверяет права тоже: если ~/.ssh доступен на запись группе или всем, sshd не принимает ключи из authorized_keys, даже верные. Ошибка видна только в журнале сервера. Чтение журнала требует прав администратора. На обычном сервере строки об отказе показывает команда ниже; приведённые строки получены на тестовом sshd, запущенном без root на порту 2222 со своим журналом, после chmod 777 ~/.ssh, поэтому это пример вывода. В WSL2 без systemd тот же текст ищут в файле /var/log/auth.log командой sudo grep sshd /var/log/auth.log | tail -3.

sudo journalctl -u ssh -n 3
sshd[2231]: Authentication refused: bad ownership or modes for directory /home/student/.ssh
sshd[2231]: Failed publickey for student from 192.0.2.20 port 51422 ssh2: ED25519 SHA256:g5V5rEe61Iakz6Rk0cbScSVf/6v9I8oT9dKDi6kZ/5M
sshd[2231]: Connection closed by authenticating user student 192.0.2.20 port 51422 [preauth]

Строка bad ownership or modes указывает на неверные владельца или права каталога (проверку выполняет директива StrictModes yes, включённая по умолчанию). Клиент в выводе ssh -v видит лишь Offering public key: ..., затем снова Authentications that can continue: publickey,... и итоговый Permission denied: причину отказа сервер клиенту не сообщает; такая поломка разбирается в лабораторной работе.

Главный инструмент диагностики на клиенте — опция -v (максимум -vvv). Для иллюстрации подключение выполнено к адресу, где ничего не слушает:

ssh -v -o ConnectTimeout=3 127.0.0.1
OpenSSH_8.9p1 Ubuntu-3ubuntu0.17, OpenSSL 3.0.2 15 Mar 2022
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
debug1: /etc/ssh/ssh_config line 21: Applying options for *
Pseudo-terminal will not be allocated because stdin is not a terminal.
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.
debug1: connect to address 127.0.0.1 port 22: Connection refused
ssh: connect to host 127.0.0.1 port 22: Connection refused

Строки debug1: идут в порядке выполнения шагов. Отказ при подключении относится к службе: адрес доступен, но sshd не слушает порт. Отказ после подключения относится к аутентификации; например, строка Load key ... bad permissions появляется уже после установки соединения. Ответ сервера при неверном ключе выглядит примерно так:

student@192.0.2.10: Permission denied (publickey,password).

Строка Permission denied (publickey,password) означает, что сервер не принял ни один метод. Причиной может быть ключ, не добавленный в authorized_keys, или неверные права на сервере.

Удобства и передача данных по SSH

Файл ~/.ssh/config и ssh-agent

Файл ~/.ssh/config задаёт для сервера короткий псевдоним и параметры подключения. Пример:

Host lab
    HostName 192.0.2.10
    User student
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Host *.lab
    User admin

Host задаёт псевдоним, HostName — адрес, User, Port, IdentityFile — параметры подключения, IdentitiesOnly yes запрещает пробовать остальные ключи. Последний блок — шаблон для имён на .lab. Значения читаются сверху вниз, первое совпадение побеждает.

Опция -G печатает итоговую конфигурацию без подключения. Пример использует демонстрационный файл ./config:

ssh -G -F ./config lab | grep -E '^(hostname|user|port|identityfile|identitiesonly) '
user student
hostname 192.0.2.10
port 22
identitiesonly yes
identityfile ~/.ssh/id_ed25519

Если подключение идёт не туда, первым делом стоит проверить вывод -G.

Агент ssh-agent держит расшифрованный закрытый ключ в памяти, избавляя от повторного ввода парольной фразы. Переменные, которые он выводит, применяет к оболочке eval. Пример с демонстрационным ключом:

eval "$(ssh-agent -s)"
ssh-add -l
ssh-add /tmp/keys-demo/id_ed25519
ssh-add -l
eval "$(ssh-agent -k)"
echo "SSH_AGENT_PID=${SSH_AGENT_PID:-пусто}"
Agent pid 24346
The agent has no identities.
Identity added: /tmp/keys-demo/id_ed25519 (student@lab)
256 SHA256:g5V5rEe61Iakz6Rk0cbScSVf/6v9I8oT9dKDi6kZ/5M student@lab (ED25519)
Agent pid 24346 killed
SSH_AGENT_PID=пусто

ssh-add -l перечисляет ключи агента. ssh-agent -k завершает агент и печатает команды unset, но сам их не выполняет: дочерний процесс не может изменить окружение родителя (тема 8). Поэтому, как и при запуске, вывод передают оболочке через eval "$(ssh-agent -k)"; последняя строка примера подтверждает, что переменная очищена. Без eval команда лишь напечатала бы строки unset SSH_AUTH_SOCK; и unset SSH_AGENT_PID;, а переменные остались бы в оболочке.

Копирование файлов: scp, sftp и rsync

Команда scp копирует файлы через SSH; путь к удалённой машине записывается как пользователь@адрес:путь. Команда sftp открывает интерактивный сеанс передачи файлов по тому же SSH-соединению: внутри доступны команды ls, cd, pwd, get (скачать), put (загрузить) и bye (выход), а команды с префиксом l (lls, lcd) относятся к локальной машине. Это удобно, когда нужно сначала посмотреть содержимое каталогов на сервере. Пример вывода, так как сервера в среде нет:

sftp lab
Connected to lab.
sftp> put note.txt
Uploading note.txt to /home/student/note.txt
note.txt                                      100%   15    12.1KB/s   00:00
sftp> ls -l note.txt
-rw-rw-r--    1 student  student        15 Oct 12 10:20 note.txt
sftp> bye

Ключ -b файл выполняет команды sftp из файла без диалога, что пригодится в скриптах. С версии OpenSSH 9.0 (Ubuntu 24.04) scp по умолчанию тоже работает через протокол SFTP, а синтаксис команды не изменился. Для регулярной синхронизации удобнее rsync -avz: -a сохраняет права, времена и структуру каталогов, -v выводит список файлов, -z сжимает данные. rsync передаёт только изменившиеся части.

Сначала поведение rsync рассмотрим локально:

mkdir -p src/docs && echo 'строка' > src/index.html && echo 'текст' > "src/docs/Отчёт 1.txt" && rsync -avz src/ dst/
sending incremental file list
index.html
docs/
docs/Отчёт 1.txt

sent 265 bytes  received 66 bytes  220.67 bytes/sec
total size is 24  speedup is 0.07

Важна деталь: завершающий слеш в src/. С ним копируется содержимое каталога, без него — сам каталог внутрь приёмника. Проверим ошибку:

rsync -avz src dst3/ && find dst3 | sort
sending incremental file list
created directory dst3
src/
src/index.html
src/docs/
src/docs/Отчёт 1.txt

sent 279 bytes  received 97 bytes  752.00 bytes/sec
total size is 24  speedup is 0.06
dst3
dst3/src
dst3/src/docs
dst3/src/docs/Отчёт 1.txt
dst3/src/index.html

Здесь src оказался внутри dst3. Перед реальной синхронизацией полезно проверять результат ключом -n (англ. dry run): rsync печатает действия, но ничего не записывает. Повторный запуск без изменений показывает пустой список:

rsync -avz src/ dst/
sending incremental file list

sent 138 bytes  received 13 bytes  302.00 bytes/sec
total size is 24  speedup is 0.16

Пустой список означает, что приёмник уже совпадает с источником.

Та же команда через SSH; сервера в среде нет, поэтому показан реальный отказ:

rsync -avz -e "ssh -o ConnectTimeout=3" note.txt 127.0.0.1:/tmp/
ssh: connect to host 127.0.0.1 port 22: Connection refused
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: unexplained error (code 255) at io.c(232) [sender=3.2.7]

На работающем сервере (проверено на тестовом sshd, порт 2222 указан для псевдонима lab в ~/.ssh/config) та же команда передаёт файл:

rsync -avz note.txt lab:/home/student/
sending incremental file list
note.txt

sent 125 bytes  received 35 bytes  320.00 bytes/sec
total size is 15  speedup is 0.09

Опция -e задаёт программу канала. Причину отказа rsync лишь передаёт от ssh, поэтому смотреть нужно первую строку. Команда scp даёт тот же отказ:

scp note.txt 127.0.0.1:/tmp/
ssh: connect to host 127.0.0.1 port 22: Connection refused
lost connection

Монтирование удалённого каталога: sshfs

Команды scp, sftp и rsync копируют файлы: после передачи на ноутбуке лежит отдельная копия, и изменения в ней не попадают на сервер, пока их не скопируют обратно. Иногда удобнее другой подход — работать с файлами сервера так, будто они лежат на локальном диске: открыть их в привычном редакторе, просмотреть less, обработать конвейером из темы 5. Эту задачу решает программа sshfs (англ. SSH Filesystem). Она монтирует удалённый каталог в локальный, а каждое обращение к файлу превращает в запрос SFTP по обычному SSH-соединению. Монтирование как таковое рассматривалось в теме 10: каталог-точка монтирования показывает содержимое другой файловой системы.

Обычно монтирование требует прав root, но sshfs работает через механизм FUSE (англ. Filesystem in Userspace): драйвер файловой системы выполняется как обычная программа пользователя, а ядро лишь передаёт ей обращения к файлам. Поэтому монтировать и отмонтировать каталог может сам пользователь, без sudo. Пакет устанавливается один раз:

sudo apt install sshfs

Смонтируем каталог www с сервера lab (псевдоним из ~/.ssh/config, вход по ключу) в пустой локальный каталог ~/remote-www. Сначала указывается удалённый каталог в той же форме, что у scp, затем точка монтирования:

mkdir -p ~/remote-www
sshfs lab:/home/student/www ~/remote-www
echo "код: $?"
ls -l ~/remote-www
код: 0
total 8
drwxr-xr-x 1 student student 4096 Oct 18 18:51 docs
-rw-r--r-- 1 student student   35 Oct 18 18:51 index.html

Команда ничего не печатает при успехе и сразу возвращает управление: sshfs уходит в фоновый режим и обслуживает обращения к каталогу. Что именно смонтировано, показывает команда findmnt:

findmnt ~/remote-www
TARGET                   SOURCE                 FSTYPE     OPTIONS
/home/student/remote-www lab:/home/student/www  fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000

Тип файловой системы fuse.sshfs и источник lab:/home/student/www подтверждают, что каталог принадлежит серверу. Запись в смонтированный каталог сразу попадает на сервер: после команды echo 'правка' >> ~/remote-www/index.html строка появляется в файле на удалённой машине без отдельного копирования. Отмонтирует каталог сам пользователь, командой fusermount -u (без прав root, в отличие от umount):

fusermount -u ~/remote-www
echo "код: $?"
ls ~/remote-www
код: 0

После отмонтирования ~/remote-www снова пустой локальный каталог.

У удобства есть цена, и её нужно понимать. Каждое чтение и каждая запись — это обмен сообщениями по сети, поэтому команды, которые обходят много файлов (find, grep -r, du), работают через sshfs в десятки раз медленнее, чем на сервере. Для таких задач правильнее выполнить команду на самом сервере (ssh lab 'grep -r ...') или синхронизировать каталог rsync. Опаснее другое: если сеть пропала или сервер перестал отвечать, любая команда, обратившаяся к точке монтирования, зависает в ожидании ответа и не прерывается даже по Ctrl+C, а вместе с ней может зависнуть и оболочка, в которой текущий каталог находится внутри точки монтирования. Опции -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 заставляют sshfs замечать обрыв и переподключаться, а зависшую точку можно отцепить командой fusermount -uz (отложенное отмонтирование). Поэтому sshfs хорош для интерактивной работы с несколькими файлами, а в скриптах и для регулярной передачи данных надёжнее rsync.

Сравнение способов работы с удалёнными файлами сведено в таблицу.

СпособЧто получаетсяКогда использовать
scp, sftpотдельная копия файларазовая передача нескольких файлов
rsync -avzсинхронизированная копия каталога, передаются только изменениярегулярное резервное копирование, публикация сайта, скрипты
sshfsудалённый каталог виден как локальный, копии нетинтерактивная правка файлов сервера локальными программами

Проброс портов: ssh -L

Служба, слушающая 127.0.0.1:8080 на сервере, недоступна снаружи. Проброс портов (англ. port forwarding) открывает её на ноутбуке: опция -L создаёт локальный порт, соединения на который идут через туннель. Форма записи: -L локальный_порт:адрес_назначения:порт_назначения. Например, ssh -L 8081:127.0.0.1:8080 student@192.0.2.10 означает: порт 8081 на ноутбуке соединён с адресом 127.0.0.1:8080 на сервере. Схема показана на рисунке.

Проброс порта по SSH: браузер на ноутбуке обращается к localhost:8081, sshd на сервере передаёт данные на 127.0.0.1:8080
Туннель SSH: адрес 127.0.0.1:8080 понимается на сервере, поэтому доступ к нему есть только с сервера

Как показано на рисунке, адрес 127.0.0.1 в правой части понимается на сервере. Проверить директиву можно опцией -G:

ssh -G -L 8080:localhost:8080 -F ./config lab | grep -i '^localforward'
localforward 8080 [localhost]:8080

Опции -N -f запускают туннель в фоне без оболочки. Проверка на тестовом сервере, где на 127.0.0.1:8080 работает python3 -m http.server:

ssh -f -N -L 8081:127.0.0.1:8080 lab && curl -s http://127.0.0.1:8081/
служба на сервере

Туннель работает, пока жив фоновый процесс ssh; его завершают командой kill по номеру из pgrep -f 'ssh -f -N -L 8081'. Обратный проброс -R работает наоборот: порт открывается на сервере, а соединения на него передаются через туннель на машину клиента. Форма записи та же: -R удалённый_порт:адрес_назначения:порт_назначения, но адрес назначения понимается уже на стороне клиента. Например, команда ssh -N -R 9000:127.0.0.1:8080 lab открывает на сервере порт 9000, и запрос curl http://127.0.0.1:9000/, выполненный на сервере, попадёт к веб-службе на ноутбуке. Так показывают коллеге с сервера службу, работающую за NAT на ноутбуке. По умолчанию sshd открывает такой порт только на петлевом адресе сервера; чтобы он стал доступен из сети, на сервере нужна директива GatewayPorts в sshd_config. Включать её без необходимости не следует: обратный туннель с внешним адресом открывает в интернет службу, которая на это не рассчитана, в обход межсетевого экрана.

Безопасность sshd

Вход по паролю уязвим для подбора. Файл /etc/ssh/sshd_config позволяет разрешить только вход по ключу:

PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin no

PasswordAuthentication no запрещает пароли, PubkeyAuthentication yes разрешает ключи, PermitRootLogin no запрещает вход под root. Команда требует прав администратора, поэтому приводится пример:

sudo sshd -t && sudo systemctl reload ssh

Проверка sshd -t при успехе ничего не выводит; при ошибке показывает строку с проблемой, и перезагрузка не выполняется. Пример вывода ошибки:

/etc/ssh/sshd_config line 3: Bad configuration option: PasswordAuthetication

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

В Ubuntu 24.04 sshd по умолчанию запускается по требованию через ssh.socket: systemd сам слушает порт 22 и запускает ssh.service при первом подключении. Поэтому до первого входа systemctl status ssh показывает inactive (dead) и строку TriggeredBy: ● ssh.socket, и это нормально. Порт в этом режиме берётся из sshd_config при генерации сокета, поэтому после смены Port нужно выполнить sudo systemctl daemon-reload и sudo systemctl restart ssh.socket. В Ubuntu 22.04 сокета нет, и после изменения достаточно sudo systemctl restart ssh.

В WSL2 без включённого systemd службу можно запустить командой sudo service ssh start, а журнал sshd пишется в файл /var/log/auth.log (если установлен rsyslog), а не в journald. Если порт 22 занят Windows, директивой Port в sshd_config выбирают другой порт, например 2222, и указывают его в ~/.ssh/config. Сетевые особенности WSL2 описаны в документации Microsoft 7.

Межсетевой экран хоста: фильтрация трафика через nftables и iptables

Зачем серверу собственный межсетевой экран

В корпоративной инфраструктуре сетевой периметр защищён аппаратными брандмауэрами. Однако полагаться только на внешний периметр опасно:

  1. Горизонтальное перемещение (Lateral Movement): если злоумышленник скомпрометировал тестовый сервер или рабочую станцию сотрудника внутри локальной сети, он может беспрепятственно атаковать соседние серверы по локальным портам (например, СУБД или Redis без пароля).
  2. Ошибки конфигурации сервисов: разработчик может случайно запустить отладочный сервис с привязкой ко всем интерфейсам (0.0.0.0), открыв доступ к закрытым API.
  3. Облачные виртуальные машины: серверы в облаке часто имеют публичные IP-адреса и подвергаются непрерывному автоматизированному сканированию из Интернета.

Межсетевой экран хоста (англ. Host-based Firewall) реализует принцип эшелонированной защиты: сервер самостоятельно контролирует входящие и исходящие пакеты независимо от сетевой топологии 28.

Базовый защитный профиль сервера в nftables

В современном ядре Linux фильтрацией управляет подсистема netfilter через интерфейс nftables (заменивший классический iptables) 8. Рекомендуемый профиль безопасности любого Linux-сервера строится по принципу запрета по умолчанию (default drop):

# 1. Создать таблицу для протоколов IPv4 и IPv6:
sudo nft add table inet filter

# 2. Создать входящую цепочку с политикой сброса по умолчанию:
sudo nft 'add chain inet filter input { type filter hook input priority 0; policy drop; }'

# 3. Разрешить весь трафик на локальном интерфейсе замыкания (loopback):
sudo nft add rule inet filter input iif lo accept

# 4. Пропускать пакеты уже установленных и связанных соединений (conntrack):
sudo nft add rule inet filter input ct state established,related accept

# 5. Разрешить входящие подключения SSH с защитой от перебора (не более 5 попыток в минуту):
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 5/minute accept

# 6. Разрешить входящий веб-трафик (HTTP/HTTPS), если на сервере работает веб-сайт:
sudo nft add rule inet filter input tcp dport { 80, 443 } ct state new accept

# 7. Разрешить служебные сообщения ICMP (ping и Path MTU Discovery):
sudo nft add rule inet filter input ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept

Ключевой элемент конфигурации — правило ct state established,related accept: оно активирует механизм отслеживания соединений ядра (connection tracking). Сервер проверяет правила только для новых (new) пакетов; все ответы на исходящие запросы сервера (например, при обновлении пакетов через apt update) пропускаются автоматически без открытия диапазона входящих портов.

Упрощённое управление брандмауэром: утилита ufw

Для повседневного администрирования в дистрибутивах Ubuntu и Debian применяется надстройка UFW (Uncomplicated Firewall), транслирующая высокоуровневые команды в правила ядра:

sudo ufw default deny incoming       # запретить все входящие соединения
sudo ufw default allow outgoing      # разрешить исходящие соединения
sudo ufw allow 22/tcp                # разрешить порт SSH
sudo ufw limit ssh                   # включить защиту SSH от подбора (drop при частых попытках)
sudo ufw allow 80/tcp                # открыть HTTP
sudo ufw allow from 192.168.1.0/24 to any port 5432 proto tcp # доступ к PostgreSQL только из доверенной подсети
sudo ufw enable                      # активировать правила и автозагрузку
sudo ufw status verbose              # проверить активные правила

Команда ufw status numbered выводит правила с порядковыми номерами, что позволяет безопасно удалять ошибочные записи командой sudo ufw delete <номер> без риска заблокировать собственный доступ по SSH.

Итоги темы

Сетевая часть темы опирается на три понятия: адрес, порт и имя. Адрес указывает компьютер, порт — службу на нём, а имя переводится в адрес через DNS. Команды ip, ss, curl, dig и nc позволяют проверить каждый из этих уровней, и порядок диагностики важен: сначала имя, затем адрес и порт, и только потом содержимое. Сообщение Connection refused и сообщение о неизвестном имени требуют разных действий 3.

SSH сочетает аутентификацию по ключу, проверку ключа сервера через known_hosts и зашифрованный канал. Закрытый ключ остаётся на клиенте, открытый попадает в authorized_keys, а права на файлы определяют, будет ли ключ принят. Файл ~/.ssh/config, ssh-agent, rsync поверх SSH и проброс портов -L делают повседневную работу с удалённым сервером удобной и воспроизводимой 45. Программа sshfs монтирует удалённый каталог через SSH без прав root и удобна для интерактивной правки нескольких файлов, но для регулярной передачи данных и в скриптах надёжнее rsync.

Следующая тема 12 остаётся на одной машине, но разделяет её на изолированные части: пространства имён, в том числе сетевое, и группы управления ресурсами — механизмы, из которых построены контейнеры. Затем курс переходит к программированию на Bash. Умение копировать каталоги через rsync по SSH понадобится в теме 16 при резервном копировании по расписанию: сценарий запускается на одной машине, а данные сохраняются на другой 6.

Литература

  1. Shotts W. The Linux Command Line: A Complete Introduction. 2nd ed. — No Starch Press, 2019. Открытая версия: https://linuxcommand.org/tlcl.php
  2. Nemeth E., Snyder G., Hein T. R., Whaley B., Mackin D. UNIX and Linux System Administration Handbook. 5th ed. — Addison-Wesley, 2017.
  3. The Linux man-pages project. — https://man7.org/linux/man-pages/
  4. Barrett D. J., Silverman R. E., Byrnes R. G. SSH, The Secure Shell: The Definitive Guide. 2nd ed. — O'Reilly Media, 2005.
  5. OpenSSH Manual Pages. — https://www.openssh.com/manual.html
  6. rsync. Документация rsync. — https://rsync.samba.org/documentation.html
  7. Microsoft. Документация WSL. — https://learn.microsoft.com/windows/wsl/
  8. The netfilter.org project. nftables Documentation and HOWTOs. — 2025. https://wiki.nftables.org/