10723
Следим за новостями Linux, DevOps и ИБ, чтобы быть готовым к любым факапам. Бонусом — плотные шпаргалки и чеклисты для ежедневной работы в терминале. 📩 По всем вопросам: @chorapov Зеркало в MAX: https://max.ru/LinuxSkill РКН https://vk.cc/cMUwm4
Закрыть периметр правилами iptables и не остаться при этом за дверью вместе с атакующим.
Посмотреть, что слушает наружу
ss -tulpn
0.0.0.0:8099 users:(("python3",pid=3545,fd=3)). Первое, с чего начинается любой аудит — половина находок обычно в том, что сервис слушает 0.0.0.0 вместо 127.0.0.1.
ss -Hntu | awk '{print $5}' |
sed 's/:[^:]*$//' | sort | uniq -c | sort -rn
-H и с cut -d: -f1. Оба места ломаются. Без -H в подсчёт попадает строка заголовка, и в топе появляется несуществующий адрес Local. А cut по первому двоеточию режет IPv6: адрес [2001:db8::2]:54321 превращается в [2001. Отсечение через sed берёт только порт с конца.
iptables -A INPUT -m conntrack \
--ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -P INPUT DROP
-F, потом -P INPUT DROP, а разрешающие правила ниже. Между этими двумя строками сессия уже мертва, и до правил вы не дойдёте. Политика ставится последней, всегда.-m state там устаревший, актуален -m conntrack --ctstate.
iptables -A INPUT -p tcp --dport 22 \
! -s 10.0.0.0/24 -j DROP
iptables -A INPUT -p tcp --dport 22 \
-m connlimit --connlimit-above 3 -j REJECT
iptables -S показывает, что модуль сам дописал --connlimit-mask 32 --connlimit-saddr — лимит считается по одному адресу, а не по подсети. Если нужен другой охват, маску задавайте явно.
ip route add blackhole 192.168.10.0/24
ping вернул connect: Invalid argument, curl завершился с кодом 7. Для исходящего трафика это плюс, ошибка приходит мгновенно вместо таймаута, но маскировки не ждите.netfilter-persistent для правил, ip route — в конфигурацию сети, иначе следующий ребут откроет всё обратно.
Добавить пользователя в группу и не выкинуть его при этом из sudo
Добавить во вторичную группу
usermod -aG developers lisa
-a флаг -G не добавляет, а заменяет весь список. Было sudo,developers,ops — после usermod -G developers lisa осталось только developers. Ни ошибки, ни предупреждения, код возврата 0. Человек узнает об этом, когда в следующий раз наберёт sudo.
gpasswd -d bob developers
Removing user bob from group developers. В отличие от usermod -G, трогает ровно одну группу.
groupmod -g 2000 developers
find / -xdev -nogroup 2>/dev/null
chgrp -R developers /data
groupmod меняет запись в /etc/group, но файлы на диске остаются со старым числовым GID. У меня после смены ls -ld /data показал вместо имени голое 1004 — каталог стал ничьим. Ищите такие через find -nogroup и чините chgrp -R, иначе доступ к данным потеряется молча.
groupadd -f developers
groupadd -r pgsql
groupadd на существующей группе возвращает код 9 и роняет скрипт под set -e. Флаг -f делает операцию идемпотентной — то, что нужно для повторных прогонов Ansible или cloud-init. А -r создаёт системную группу: у меня она получила GID 996, обычная — 2002.
groupdel testers
cannot remove the primary group of user 'bob', код 8. Вторичную удалит без вопросов, и у пользователей она просто исчезнет из списка.id -nG lisa показывает актуальный список из базы, а groups в чужой открытой сессии — то, что было на момент входа. Пользователь не увидит новых групп, пока не перелогинится или не выполнит newgrp.groupadd -f?sudo, developers, ops. После usermod -G developers lisa осталась одна developers, код возврата 0, ни слова в выводе. Исходник называет это в «честном минусе», но в самом пункте 7 стоит только скобка «обязательно -a» — я вынес разбор в тело, потому что это главное содержание всей темы.developers с 1004 на 2000 — каталог /data стал показывать 1004 вместо имени. Формально данные не потеряны, но группа к ним больше не имеет отношения. Исходник даёт команду без единой оговорки. Добавил find -nogroup и chgrp -R как лечение.grep /etc/group. Флага -f в исходнике нет.cannot remove the primary group of user 'bob', код 8. Проверил обе ветки — вторичная удаляется молча.groups, id, chgrp, find -group, newgrp, groupmod -n, gpasswd -A как отдельные блоки — по ним нечего добавить сверх справки. Мысль про groups против id вплёл в текст: она объясняет, почему пользователь «не видит» новую группу.
🎥 Вебинар: «Типовые задачи с RAID-массивами: создание, эксплуатация, перенос данных и восстановление»
На открытом уроке разберем повседневные задачи, возникающие при работе с RAID-массивами: замену дисков, перенос данных на другой сервер и восстановление массива после сбоя. Обсудим, как собрать RAID из оставшихся дисков и в каких случаях восстановление данных возможно, а когда вернуть массив уже не получится.
О чем поговорим:
— Как создавать и эксплуатировать RAID-массивы
— Как правильно менять вышедшие из строя диски
— Как собрать массив из оставшихся рабочих дисков
— Как перенести данные на другой сервер
— Как восстанавливать RAID после сбоя и оценивать вероятность успешного восстановления
🧠 Вебинар приурочен к старту курса «Администратор Linux. Продвинутый уровень». Программа постоянно обновляется с учётом современных требований рынка. Занятия проводят практикующие эксперты, которые ежедневно работают с производственной инфраструктурой. Вы изучите востребованные инструменты: Zabbix, Prometheus, Docker, Nginx, PostgreSQL, ELK, Ansible, SELinux, Bash и многие другие.
👉 Для участия зарегистрируйтесь https://otus.pw/ZrYD/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Не оставить пароль в bash_history и не удивиться, что он всё равно там оказался.
Скрыть команду от истории
mysql -u root -p'super_secret'
~/.bash_history. Работает, если в HISTCONTROL есть ignorespace. Нюанс: в Ubuntu это уже настроено, но по-разному. В /etc/skel/.bashrc стоит ignoreboth, а в root-сессии у меня было ignoredups:ignorespace. Проверьте своё:
echo "$HISTCONTROL"
systemctl status nginx-confg
^confg^config
echo aaa bbb aaa — после ^aaa^XXX получилось XXX bbb aaa. Для всех вхождений другая конструкция:
!!:gs/aaa/XXX/
fc
$EDITOR, после сохранения bash её выполняет. Нюанс: порядок поиска редактора — FCEDIT, потом EDITOR, и только потом vi по умолчанию. На голом сервере без vi получите bash: vi: command not found и никакого редактирования. Заранее пропишите export FCEDIT=nano или что вам ближе.
reset
clear только прокручивает экран, а reset переинициализирует терминал целиком. Если и это не помогает — stty sane возвращает настройки ввода, когда не видно набираемых символов.
bg
disown -h %1
-h и без него — разные вещи. С -h задача остаётся в jobs, но не получит SIGHUP при закрытии терминала. Без флага задача исчезает из таблицы совсем. Проверил оба варианта, разница именно в этом.ps -eo args любому пользователю. Пароль в командной строке остаётся паролем в командной строке — читайте его из файла или переменной окружения.fc или сразу пишете в файл?
🎥 Вебинар: Где Linux хранит настройки и логи: разбираем файловую структуру на практике.
На открытом уроке разберем структуру каталогов Linux и узнаем, где искать конфигурационные файлы, системные журналы и другие важные данные. Рассмотрим назначение основных директорий и вспомним, как права доступа влияют на работу с файлами.
Данный открытый урок проходит в рамках курса «Администратор Linux. Базовый уровень».
О чем поговорим:
— Как устроена файловая структура Linux
— Для чего предназначены основные системные каталоги
— Что такое конфигурационный файл и где обычно хранятся настройки сервисов
— Где Linux хранит системные логи и как найти нужный журнал
— Как права доступа влияют на чтение и изменение файлов
🧠 Вебинар приурочен к старту курса «Администратор Linux. Базовый уровень». Вы освоите Bash, TCP/IP, Nginx, Apache, MySQL, Docker, Git, Prometheus, Grafana и ELK. На живых занятиях с практикующими экспертами научитесь настраивать веб-серверы, работать с сетью, контейнерами, мониторингом и фильтрацией трафика.
👉 Для участия зарегистрируйтесь https://otus.pw/e3sf/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Выкинуть из скрипта форки awk и cut и не нарваться на подстановку, которая молчит
Разобрать путь без basename и dirname
path="/var/log/nginx/access.log.tar.gz"
name="${path##*/}" # access.log.tar.gz
dir="${path%/*}" # /var/log/nginx
basename — 1107 мс, подстановка — 5 мс. Разница в двести раз, и это не про скорость самой утилиты, а про форк процесса на каждый вызов.
shopt -s nullglob
files=(/var/log/nginx/*.log)
cp -- "${files[@]}" /backup/logs/
nullglob массив по несуществующей маске не пустой. Проверил — в нём оказался один элемент, сама строка /var/log/nginx/*.nosuch, и cp уедет с несуществующим файлом. shopt -s nullglob заставляет пустой glob разворачиваться в ноль элементов.
diff <(ls -1 dir1) <(ls -1 dir2)
<(...) подставляет путь вида /dev/fd/63, за которым стоит обычный pipe — проверил через ls -l. Нюанс: код возврата команды внутри скобок теряется, diff его не увидит. И это только bash: в dash тот же синтаксис даёт Syntax error, так что для #!/bin/sh не годится.
: "${TIMEOUT:=30}"
: "${DB_USER:?не задана}"
:? обязательно. Вариант ${DB_USER?...} из подборок срабатывает только когда переменная не объявлена вовсе — пустую строку он пропускает, и скрипт идёт дальше с пустым логином. С :? тот же случай падает с кодом 127. То же и с :-: он подставляет дефолт, но не присваивает, переменная остаётся пустой.
PS4='+ ${EPOCHREALTIME} '
set -x
PS4='+ $(date "+%s.%N") => ' — и это форк date на каждую строку трассировки, в посте про избавление от форков. Встроенная EPOCHREALTIME даёт то же время с микросекундами и без единого процесса.
🎥 Вебинар: «LVM без простоя: расширение тома, перенос данных и аварийный откат через snapshot»
На открытом уроке разберем, как использовать возможности LVM при работе с хранилищем в production-среде. Рассмотрим расширение логического тома, перенос данных на другое блочное устройство и применение snapshot для возврата к предыдущему состоянию системы.
О чем поговорим:
— Как устроен LVM и какие задачи он помогает решать
— Как расширять логические тома при увеличении объема данных
— Как перенести данные на другое блочное устройство
— Что такое snapshot и как он работает
— Как использовать snapshot для отката при возникновении проблем
🧠 Вебинар приурочен к старту курса «Администратор Linux. Продвинутый уровень». Программа постоянно обновляется с учётом современных требований рынка. Занятия проводят практикующие эксперты, которые ежедневно работают с производственной инфраструктурой. Вы изучите востребованные инструменты: Zabbix, Prometheus, Docker, Nginx, PostgreSQL, ELK, Ansible, SELinux, Bash и многие другие.
👉 Для участия зарегистрируйтесь https://otus.pw/T9OrR/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Сервис упал ночью, до утра гигабайт логов. Порядок, в котором в них лезть
Начать с ошибок текущей загрузки
journalctl -p err -b
-b отсекает всё, что было до последней перезагрузки, -p err оставляет уровень Error и выше. Цифровой вариант -p 3 делает то же самое, но словом читается лучше.
journalctl --since "2026-08-13 23:00:00" \
--until "2026-08-14 01:00:00"
yesterday 23:00:00, которые кочуют по подборкам, systemd не разбирает — получишь Failed to parse timestamp. Проверил на systemd 255: отдельно yesterday работает, -2h работает, 23:00 работает, а вот их комбинация — нет. Надёжнее всегда писать полную дату.
dmesg -T --level=err,warn
dmesg -T | grep -i oom-killer
-T переводит секунды аптайма в нормальную дату. Фильтр по уровню полезнее грепа: покажет и OOM, и ошибки диска, и отвалившийся сетевой интерфейс.
ausearch -m avc -ts recent
/sys/fs/selinux просто нет. Шаг актуален для RHEL-семейства; на Debian смотри journalctl -t audit и dmesg | grep -i apparmor.
awk -F'"' '{split($3,a," ");
if (a[1]==404) print $1}' access.log |
awk '{print $1}' | sort | uniq -c | sort -rn
awk '$9 == 404' разваливается, если в URL попал пробел — поле уезжает, и строка молча не считается. У меня из трёх запросов с 404 такой вариант нашёл два. Разбор по кавычкам берёт код ответа там, где он реально лежит.
Как достать пароль из kdbx, когда графика легла и буфер обмена недоступен
Всё проверено на keepassxc-cli 2.7.6 из репозитория Ubuntu 24.04.
Посмотреть, что вообще есть в базе
keepassxc-cli ls -R database.kdbx
-R увидишь только верхний уровень. Вложенные записи в группах не покажет — легко решить, что база пустая.
keepassxc-cli show -a Password database.kdbx "Production/DB"
show без ключей выводит Password: PROTECTED вместо значения. Флаг -s раскрывает все поля, -a Password отдаёт только пароль одной строкой — удобно подставлять в скрипт.
keepassxc-cli ls -k keyfile.key database.kdbx
-k работает во всех подкомандах. Если пароля на базе нет вообще, добавь --no-password, иначе утилита будет ждать ввода.
keepassxc-cli search database.kdbx "DB"
/Production/DB, который дальше подставляется в show.
keepassxc-cli export -f csv database.kdbx
clip, который советуют для копирования пароля в буфер. Именно в сценарии «GUI лёг» он и не работает: без графической сессии команда отвечает All clipping programs failed. Tried xclip. Смысл в нём есть только в живой графике, где он же и чистит буфер через 10 секунд..kdbx.lock. Утилита о нём знает, так что «гарантированного повреждения» не будет — но менять базу из двух мест одновременно всё равно не стоит, последняя запись затрёт чужие правки.
Пробросить TCP через WebSocket мимо DPI и не собрать по дороге утечку сокетов
websocat — консольный клиент вебсокетов на Rust, авторства Виталия Шукелы. Соединение начинается с обычного HTTP-рукопожатия Upgrade, поэтому фильтр видит легитимный веб-трафик. Проверял на версии 1.14.0, скачанной статическим бинарником с релизов.
Пробросить локальный TCP-порт в вебсокет
websocat --binary -E \
tcp-l:127.0.0.1:8080 ws://example.com/socket
-E websocat печатает предупреждение про утечку сокетов при обслуживании нескольких клиентов. У меня оно вылезало на каждом запуске туннеля, с флагом лог чистый.
websocat -t --ping-interval 10 \
- autoreconnect:ws://example.com/socket
autoreconnect: требует двух аргументов. Вариант с одним, который ходит по подборкам, падает: Specify ws:// or wss:// URI to connect to a websocket. Первым аргументом идёт источник данных — - для stdin или tcp-l: для туннеля.Reconnecting failed, процесс жив. Сервер появился — соединение поднялось само.
echo "get_metrics" | websocat -1 ws://example.com/socket
get_metrics и завершился с кодом 0.
websocat -k wss://staging.example.com/socket
-k отключает проверку сертификата. Для стенда годится, дальше стенда — нет: MITM на таком туннеле не отличить от обычной работы.websocat -t ws-l:127.0.0.1:1234 mirror: поднимает локальный эхо-сервер, на котором можно проверить свою команду, не выходя наружу.overlay:addrtype:address, и с непривычки половина попыток заканчивается Invalid command-line parameters. Плюс инкапсуляция в кадры съедает скорость, а на больших объёмах приходится крутить -B вручную.
Посмотреть, какие интерфейсы подняты, и не принять погасший порт за живой
Краткий статус интерфейсов
ip -4 -br address show up
up фильтрует по состоянию линка, а не по наличию адреса. Погасил eth0 — с ключом он пропал из вывода совсем, без ключа виден как DOWN со всеми адресами на месте. Пустая строка тут не значит «адреса нет».
ip address add 10.0.0.50/24 dev eth0
ip address del 10.0.0.50/24 dev eth0
ip route get 8.8.8.8
# 8.8.8.8 via 192.0.2.1 dev eth0 src 192.0.2.2
local 192.0.2.2 dev lo.
ip route show table all
show дал одну строку, table all — шесть, включая local с broadcast и host-маршрутами. С VPN и контейнерами разница уходит в десятки строк.
ip -4 neigh show
ip -6 neigh show
ip neigh — не ARP-таблица, как пишут в шпаргалках, а таблица соседей: ARP плюс IPv6 NDP. Без ключа -4 или -6 получишь обе вперемешку. arp -n показывал только первую, отсюда и путаница.
ip -s -s neigh flush dev eth0
-s -s команда молчит и отдаёт код 0 — не отличишь «очистил десять записей» от «там было пусто». С ключом печатает удалённые записи и итог *** Round 1, deleting 1 entries ***.
ip link property add dev eth0 altname eno2
altname eno2 появляется в ip link show eth0, и по новому имени интерфейс находится: ip address show dev eno2 отдаёт eth0. Есть с ядра 5.8 и iproute2 5.8, на RHEL 7 синтаксис не разберётся.CAP_NET_ADMIN — в контейнере без --cap-add=NET_ADMIN упадёт.ip route get в отладке применяете или сразу лезете в show?
Попалась заметка про reptyr — утилиту, которая забирает уже запущенный процесс в новую сессию терминала. Команды из ходовых инструкций прогнал сам, на двух машинах с Ubuntu 24.04.
Ситуация: запустил в ssh дамп базы, а он затянулся. Надо было сразу в tmux, но уже поздно.
Сначала отвязываем процесс от текущей оболочки:
# Ctrl-Z, потом
bg
jobs -l
disown %1
disown top, по имени команды. Оно работает, но ровно пока такой job один. Проверил: один sleep снимается нормально, два — ambiguous job spec и выход с кодом 1. Пиши %1 или PID, не имя.# PID берём из вывода jobs -l
reptyr 7972
top из одной tmux-сессии в другую: нулевой дескриптор сменился с /dev/pts/0 на /dev/pts/2, вывод продолжился в новом окне.[-] Timed out waiting for child stop. — и всё равно перенёс. Причём это вылезло на обеих машинах и на разных версиях, так что предупреждение можно игнорировать.kernel.yama.ptrace_scope равен 1: подцепиться можно только к своему потомку. Процесс из старой ssh-сессии новой оболочке не потомок. Unable to attach to pid 2162:
Operation not permitted
ptrace_scope — за это спасибо автору. Лечится так:cat /proc/sys/kernel/yama/ptrace_scope
# 0 — классические правила ptrace
sysctl -w kernel.yama.ptrace_scope=0
/dev/pts/5. Но это ослабление защиты: любой процесс под тем же uid сможет подцепиться к любому другому. Держать так постоянно на боевой машине я бы не стал. Под root, кстати, всё работает и при единице — CAP_SYS_PTRACE обходит yama.-T: крадёт весь терминальный сеанс и по описанию должен выручать, когда у процесса есть дети. Взял конвейер sleep 400 | cat, натравил reptyr -T — и ничего. Ни ошибки, ни переноса: лог пустой, tty прежний. В чём подвох, не разобрался.reptyr --help отвечает invalid option.
🔬 Дали root и разрешили ломать. Сервис, который я хотел создать
Постоянные читатели знают, что у меня есть бот @gradeliftbot, в котором больше 200 задач по Linux. Работает он просто: кусок лога и вопрос по нему. Песочницы нет вообще. Всё скатывается в механику заданий вопрос-ответ. Кейсы генерит нейросеть, и местами это заметно, хотя там содержится достаточно много материала, чего стоит только полный гайд от Docker на 1195 страниц и 8500 страниц с командами Linux и их подробным описанием.
Изначально идея была создать песочницу с заданиями, где нужно было выполнять задания, но я так и не смог придумать, как адаптировать её под мобильный формат Telegram-бота. Поэтому получилась очень упрощённая версия от изначальной идеи.
На днях наткнулся на izzylab.ru — тренажёр по Linux и DevOps, где терминал живёт прямо в браузере. Открыл первую задачу «создай пользователя john» и вместо useradd набрал разведку. Интереснее было понять, куда меня пустили и насколько там можно наглеть.
uname -r; nproc; free -m | head -2
id; sudo -n true; echo $?
4.19.0-gvisor, 2 ядра, 256 МБ, uid=0. Под тобой не хост и не виртуалка, а gVisor — юзерспейсное ядро от Google, которое перехватывает системные вызовы и разбирает их само. Отсюда и щедрость: тебе спокойно дают root и разрешают крушить систему, потому что до настоящего ядра ты всё равно не дотянешься. У каждой задачи свой контейнер, сброс откатывает всё начисто.
Больше всего я боялся увидеть чекер, который сверяет введённую строку с эталоном. Проверил в лоб. Задание «закодируй /root/data.bin в base64» решил не той утилитой:
openssl base64 -A -in /root/data.bin \
-out /root/data.b64
Засчитано, хотя штатный base64 ломает вывод каждые 76 символов, а openssl -A пишет одной строкой. Проверка смотрит на результат, а не на способ. Обмануть тоже не вышло: подложил валидный base64 от постороннего текста — поймал и написал, какой критерий не сошёлся, решение при этом не выдал.
На момент, когда я смотрел сервис, уже было 437 заданий, что значительно больше чем в @gradeliftbot. 87 лёгких, 276 средних, 74 сложных. Разбивка не самая ожидаемая — Git 39, регулярки 30, скрипты 29, SQLite 25, Kubernetes 20, диагностика 20. Redis и Postgres поднимаются руками и отвечают, apt install работает, сеть наружу есть.
А дальше пошли находки, ради которых стоило лезть. ss -s возвращает get_sockstat: No such file or directory, при этом ip -br a работает нормально. Причина в netlink: gVisor держит NETLINK_ROUTE, но не NETLINK_SOCK_DIAG, на котором стоит ss. Я сам писал «netstat мёртв, бери ss» — вот вам среда, где канон переворачивается.
Со strace то же самое, только тоньше. Запуск программы под трейсом работает, а attach к чужому PID — нет:
ptrace(PTRACE_SEIZE, 261): Operation not permitted
PTRACE_TRACEME есть, PTRACE_SEIZE нет. Ещё ping даёт 100% потерь, а curl https://ya.ru возвращает 302 — ICMP не проброшен, TCP ходит. Я такие штуки люблю: за час разведки узнал про gVisor больше, чем за все статьи о нём.
Теперь честно. Если вы хотите просто научиться внимательно читать логи, что важно на собесах или когда за кем-то исправляешь ошибки, тогда подходит @gradeliftbot. А если цель — именно практика в живом терминале, то формат izzylab.ru на мой взгляд, подходит лучше.
Что скажете насчёт идеи живой песочницы в браузере телефона? Тут в MAX ботов разрешили создавать самозанятым, можно будет попробовать перенести бота из телеги в MAX.
#Linux #DevOps #gVisor #strace #Практика
🧨 Ты ищешь в strace open() — а его там нет
Полез за выжимкой по strace в свою базу конспектов, получил бодрый совет: фильтруй по -e trace=open,openat, увидишь, где программа ищет конфиг. Решил проверить перед постом. Ubuntu 24.04, strace 6.8. Не увидел ничего.
Подопытный — скрипт, который проверяет конфиг через test -f:
# фильтр из конспекта
strace -f -e trace=open,openat ./finder.sh \
| grep myapp
# пусто
# весь файловый класс
strace -f -e trace=%file ./finder.sh | grep myapp
# newfstatat(AT_FDCWD, "/etc/myapp/config.ini",
# 0x7ffc0f32bee0, 0) = -1 ENOENT
# -Z печатает только вызовы, вернувшие ошибку
strace -f -Z -e trace=%file \
-o /tmp/t.log ./myapp
grep -E 'ENOENT|EACCES' /tmp/t.log
PID=$(systemctl show -p MainPID --value nginx)
sudo strace -f -Z -e trace=%file -p $PID
🎯 Забудь про дублирование путей! Профессиональная навигация в Bash
При создании директории под проект ты постоянно вводишь две команды: сначала mkdir, затем cd с тем же длинным путём. Показываю, как схлопнуть это через встроенную переменную оболочки.
Классический подход (трата времени)
# ПЛОХО: ручной повтор пути отнимает время и провоцирует опечатки
mkdir /var/www/my_new_hardcore_project
cd /var/www/my_new_hardcore_project
$_)# ХОРОШО: объединяем через && и подставляем последний аргумент через $_.
# Кавычки обязательны — иначе путь с пробелом сломает cd (too many arguments).
mkdir -p /var/www/my_new_hardcore_project && cd "$_"
&& гарантирует, что переход выполнится только при успешном создании каталога. Переменная $_ разворачивается в последний аргумент предыдущей команды. Кавычки вокруг "$_" не опциональны: без них путь с пробелом разобьётся на несколько аргументов, и cd упадёт.# ИДЕАЛЬНО: чтобы не вспоминать про && cd "$_", зашей функцию в ~/.bashrc
mkcd() {
mkdir -p -- "$1" && cd -- "$1"
}
# После source ~/.bashrc создание и переход — одна команда:
mkcd /var/www/my_new_hardcore_project
🎥 Вебинар: «Сможет ли ИИ починить Linux-сервер: где заканчиваются подсказки и начинается инженерная диагностика»
Разберем реальный инцидент с Linux-сервером и попробуем найти причину проблемы с помощью ИИ. Посмотрим, какие рекомендации действительно помогают инженеру, какой информации не хватает модели и почему некоторые команды опасно выполнять без понимания состояния системы.
О чем поговорим:
- Разберем реальный инцидент с Linux-сервером и попробуем диагностировать его с помощью ИИ.
- Проверим, какие рекомендации AI можно использовать, а какие требуют дополнительной проверки.
- Сравним готовые команды с последовательной инженерной диагностикой и поиском первопричины.
- Покажем, как безопасно применять ИИ для анализа логов, формирования гипотез и подготовки плана действий.
- Разберем формат обучения, практические задания, поддержку преподавателей и итоговый проект курса.
🧠 Вебинар приурочен к старту курса «Администратор Linux. Продвинутый уровень». Программа постоянно обновляется с учётом современных требований рынка. Занятия проводят практикующие эксперты, которые ежедневно работают с производственной инфраструктурой. Вы изучите востребованные инструменты: Zabbix, Prometheus, Docker, Nginx, PostgreSQL, ELK, Ansible, SELinux, Bash и многие другие.
👉 Для участия зарегистрируйтесь https://otus.pw/6rPu/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Собрать архив с исключениями и не обнаружить потом, что исключения не сработали.
Исключить каталог из архива
tar --exclude='data/logs' -czf arch.tgz data
--exclude обязан стоять до источника. Поставил после — tar честно предупредил --exclude 'data/logs' has no effect и всё равно упаковал логи. Второе: шаблон сравнивается с тем, как путь записан внутри архива. Упаковываете data, значит исключать надо data/logs, а не /tmp/ta/data/logs — с абсолютным путём совпадения не будет.
tar -tf arch.tar | grep app.conf
tar -xf arch.tar tmp/ta/data/conf/app.conf
Removing leading '/' from member names. Поэтому распаковать по тому же абсолютному пути не выйдет — получите Not found in archive. Сначала посмотрите список через -t, потом копируйте путь оттуда.
tar -rf arch.tar newfile.txt
tar --delete -f arch.tar oldfile.txt
.tgz обе команды падают с Cannot update compressed archives и Error is not recoverable: exiting now. Чтобы поправить сжатый архив, его придётся распаковать целиком.
tar --zstd -cf arch.tar.zst data
zstd в системе нет, tar создаст файл нулевого размера и завершится с кодом 2. Файл при этом лежит на месте и выглядит как архив — в скрипте бэкапа такое заметят не сразу. Проверяйте код возврата, а не наличие файла.
tar -xf arch.tar --keep-newer-files
Current 'data/logs/a.log' is newer or same age. Нюанс: код возврата при этом 2, как при ошибке. Под set -e скрипт упадёт, хотя всё прошло по плану.tar -df arch.tar показывает Mod time differs и Size differs по каждому расхождению. Удобно проверить бэкап, не распаковывая.
Собрать дерево каталогов одной строкой и не получить папку с фигурными скобками в имени.
Создать вложенную структуру
mkdir -p project/src/{configs,utils,tests}
sh строка отработает буквально, создав каталог с именем {configs,utils,tests} — проверил, ошибки при этом никакой. То же будет от пробела после запятой и от одного элемента без запятой: {only} останется как есть.#!/bin/sh, скобки не использовать.
cd -
cd - печатает путь в stdout. В скрипте это мусор в выводе, а $(cd -) вообще бесполезен — подстановка уходит в подоболочку, и рабочий каталог самого скрипта не меняется. Для скриптов тише так:
cd "$OLDPWD"
pushd /var/log >/dev/null
popd >/dev/null
popd возвращает directory stack empty и код 1. Под set -e скрипт на этом падает — а такое легко получить, если между pushd и popd стоит выход по ошибке. Перенаправление в /dev/null тоже нужно: обе команды печатают содержимое стека.
cd -P /var/run
cd оставляет PWD равным /tmp/dd/link, с -P он становится /tmp/dd/real. Нюанс не в самом переходе, а в том, что будет дальше. Из link/sub команда cd .. вернёт в link, а cd -P .. — в real. Скрипт, который ходит относительными путями через симлинк, легко уедет не туда. Заодно pwd -P показывает физический путь, когда pwd показывает логический.
rmdir -p a/b/c
Directory not empty — но то, что успел удалить ниже, уже удалено. Возврата нет, так что для глубоких цепочек лучше сначала посмотреть find a -type d -empty.pushd или всё-таки алиасами и cd -?
Смонтировать сетевую шару и не подвесить процессы, когда сервер отвалится.
Убрать пароль из fstab
mount -t cifs -o credentials=/etc/cifs-creds \
//10.0.0.10/share /mnt/samba
/etc/cifs-creds из трёх строк: username=, password=, domain=. Нюанс: chmod 600 на нём — целиком на вашей совести, mount права не проверяет и с файлом 644 отработает молча. Заодно проверьте, что каталог с ним не уезжает в бэкап открытым текстом.
umount -l /mnt/nfs
-l каталог пуст и монтирования в mount больше нет, но процесс, державший файл открытым, спокойно дочитал его по старому дескриптору. Нюанс: -l убирает точку из дерева, а не освобождает ресурсы — пока живы процессы, ФС остаётся. Ищите их через lsof +D до того, как отцепите, иначе искать будет негде.-f имеет смысл только для NFS и только вместе с недоступным сервером, для cifs он почти ничего не даёт.
showmount -e 10.0.0.10
showmount говорит по протоколу MOUNT, которого в NFSv4 нет вообще. На сервере с отключёнными v2 и v3 получите clnt_create: RPC: Program not registered — и это не значит, что сервер мёртв. Для v4 просто монтируйте корень экспорта: mount -t nfs4 10.0.0.10:/ /mnt.
//10.0.0.10/share /mnt/samba cifs
noauto,x-systemd.automount,_netdev,
credentials=/etc/creds 0 0
_netdev нужен наравне с x-systemd.automount. Без него systemd не знает, что монтирование сетевое, и может дёрнуть его до поднятия интерфейсов. Плюс x-systemd.idle-timeout= отцепит шару после простоя — на нестабильном канале это лучше, чем висящая точка.
mount -t nfs -o soft,timeo=50,retrans=2 \
10.0.0.10:/data /mnt/nfs
timeo в децисекундах, так что 50 — это 5 секунд. По умолчанию для TCP стоит 600, то есть минута, да ещё с линейным ростом после каждой переотправки.soft пишет прямо — silent data corruption. Приложение получит I/O error, и если оно его не обработает, запись потеряется тихо. Для баз и любой записи оставляйте hard. И не тащите из старых шпаргалок intr — опция игнорируется с ядра 2.6.25.
Найти на сервере скрытых рутов и не поверить проверке, которая их не видит.
Найти лишние учётки с нулевым UID
getent passwd | awk -F: '($3 == 0) {print $1}'
awk -F: '($3 == "0")' — со строковым сравнением. Оно пропускает UID, записанный как 00 или 0000. Проверил: завёл пользователя с -u 00, id показал uid=0(root), то есть для ядра это полноценный рут, а строковая проверка его не увидела. Без кавычек awk сравнивает числа и ловит все варианты.getent вместо cat /etc/passwd нужен, чтобы захватить пользователей из LDAP и SSSD.
sshd -T | grep -i permitrootlogin
grep по /etc/ssh/sshd_config показывает то, что написано, а не то, что действует. Собрал стенд: в основном конфиге PermitRootLogin no, а в /etc/ssh/sshd_config.d/99-cloud.conf — yes. Строка Include стоит выше, drop-in побеждает:
grep -> PermitRootLogin no
sshd -T -> permitrootlogin yes
sshd -T печатает итоговую конфигурацию после всех включений.
visudo -c -f /etc/sudoers.d/devops
visudo без аргументов знают все, а вот -c -f работает с любым файлом и годится для CI или проверки после Ansible. На кривом правиле показывает строку и место ошибки, на нормальном — parsed OK. Заодно sudo -l покажет, что реально разрешено текущему пользователю.
find / -xdev -perm /6000 -type f -ls 2>/dev/null
-perm -4000 -user root из чек-листов пропускает SGID. На моей машине разница 9 файлов против 12 — мимо проверки прошли chage, expiry, ssh-agent. Фильтр -user root тоже лишний: чужой SUID опаснее рутового, а он под этот фильтр не попадёт. И -xdev обязателен, иначе find захлебнётся на /proc.sudo -i вместо su спорить не буду, но помнить стоит другое: жёсткие правила в sudoers обходятся через less, find, awk и десяток других утилит — у всех есть побег в шелл. Разрешать по одной команде безопаснее, чем кажется, только если это не интерпретатор.
Обойти массив путей с пробелами и не получить вместо одной папки три несуществующих.
Перебрать элементы, не разорвав их по пробелам
folders=("/var/log/nginx" "/var/log/app with space")
for d in "${folders[@]}"; do
ls -ld "$d"
done
${folders[@]} обязательны. Без них тот же цикл на моём тесте выдал четыре итерации вместо двух: путь распался на /var/log/app, with и space. И не путай @ с *: под кавычками "${folders[*]}" склеивает весь массив в одну строку.
echo "${#folders[@]}"
${#folders} без [@] — это не размер массива, а длина первого элемента. У меня вернуло 8 вместо 3, и никакой ошибки.
for i in "${!sparse[@]}"; do
echo "$i = ${sparse[$i]}"
done
unset, индексы останутся с дырками. Массив с индексами 0, 5, 9 имеет размер 3 — обход по seq 0 2 найдёт один элемент из трёх. Перебирать надо индексы через ${!arr[@]}, а не числа от нуля.
readarray -t lines < /var/log/app.log
readarray -t lines < <(grep -v '^$' /var/log/app.log)
cat file | readarray -t lines не работает. Пайп запускает readarray в подоболочке, массив заполняется там и умирает вместе с ней. У меня получилось 0 элементов вместо 4, снова без ошибки. Нужен либо редирект, либо подстановка процесса < <(...).
declare -A ports=([nginx]=80 [php-fpm]=9000)
declare -A не формальность. Без него bash создаёт обычный индексный массив, все строковые ключи считаются нулём, и каждое присваивание перетирает предыдущее. Проверил: ${ports[nginx]} вернуло 9000 — значение от php-fpm.shellcheck на скриптах с массивами экономит больше времени, чем кажется.
Прокинуть скрипт и каталог на сервер и не собрать по дороге чужие абсолютные пути
Всё проверено на OpenSSH 9.6p1, подключением к живому sshd.
Увидеть, какие параметры реально применились
ssh -G user@host
Host и Match: порт, пользователя, список шифров, таймауты. Быстрее, чем перечитывать ~/.ssh/config глазами и гадать, какая секция сработала. Если сессия висит — ssh -vvv покажет, на каком этапе.
ssh user@host bash -s arg1 arg2 < ./script.sh
-s обязателен, если передаёшь аргументы. Без него bash примет arg1 за имя файла и ответит No such file or directory. И второе: интерактивный read внутри такого скрипта работать не будет — stdin уже занят самим скриптом.
ssh-keygen -f ~/.ssh/known_hosts -R '[10.0.0.1]:2222'
[host]:port в кавычках. Просто -R 10.0.0.1 строку не найдёт и молча ничего не сделает. Старый файл сохраняется рядом как known_hosts.old.
ssh user@host "tar -cf - -C /data ." | tar -xC ~/bak/
-C на удалённой стороне обязателен. Вариант tar -cf - /data утащит в архив весь абсолютный путь, и локально ты получишь ~/bak/data/... вместо содержимого. Проверил — распаковалось именно так.
ssh -f user@host "sleep 300"
ssh -f -N -L 9998:localhost:15672 user@host.
ssh-add -t 30 ~/.ssh/id_ed25519
ssh-agent команда просто скажет Could not open a connection to your authentication agent.sshpass из подборок: он кладёт пароль в аргументы процесса, где его видно в ps любому пользователю системы. Для закрытого стенда — ладно, дальше стенда лучше ProxyJump и ключи.-L руками или прописываете в конфиг?
Диски гостей раздулись, а внутри места полно. Вернуть его хосту — и не просадить запись
Проверить, поддерживает ли диск discard
lsblk --discard
hdparm -I /dev/sda | grep -i trim. На virtio-диске это не сработает вовсе — у меня вернуло Operation not permitted, потому что hdparm говорит по протоколу ATA, которого у виртуального диска нет. lsblk --discard показывает нужное для любого типа:
NAME DISC-GRAN DISC-MAX
vda 4K 1G
vdb 0B 0B
DISC-GRAN и DISC-MAX означают, что discard проходит. Нули — гипервизор его не отдаёт, дальше можно не настраивать.
fstrim -av --dry-run
fstrim -av
--dry-run: он делает всё то же самое, но без самого discard. У меня показал 0 B (dry run) trimmed on /dev/vda, а боевой запуск вернул 243.1 GiB trimmed.
systemctl enable --now fstrim.timer
systemctl list-timers fstrim.timer
/etc/cron.weekly писать не нужно — в пакете util-linux уже лежит готовый fstrim.timer с OnCalendar=weekly и разбросом запуска до 100 минут, чтобы виртуалки не пошли в discard одновременно.#!/bin/sh не выполнится: run-parts: failed to exec: Exec format error. И файл с точкой в имени, вроде fstrim.sh, run-parts молча пропустит.
UUID=xxxx / ext4 defaults,discard 0 1
discard при монтировании шлёт запрос на очистку при удалении каждого файла. Пакетный TRIM раз в неделю делает то же самое, но одним заходом и в спокойное время.
Если гоняешь ansible по сотне серверов, SSH каждый раз делает хендшейк заново
Переиспользовать одно TCP-соединение
# ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%C
ControlPersist 10m
~/.ssh/sockets надо создать заранее, сам он не появится.%C тут не для красоты. Классический %r@%h:%p на длинных именах хостов выходит за предел, и ssh отказывается работать:
ControlPath too long ('/root/.ssh/sockets/xxx...
-root@127.0.0.1:2222' >= 108 bytes)
%C даёт хеш фиксированной длины и снимает вопрос совсем.
ssh -O check user@host
Master running (pid=924). Пригодится, когда непонятно, идёт ли новая сессия через кеш или поднимает соединение с нуля.
ssh -O exit user@host
Exit request sent. и удаляет файл сокета. Нужно, когда на сервере поменялись ключи или конфиг, а мастер держит старую сессию.kill -9 — -O check стал отвечать Connection refused, но новое подключение прошло нормально: ssh увидел мёртвый сокет и поднял соединение заново.-O exit, но сначала надо догадаться, что дело в нём.Host * или включаете точечно?
Закрыть системные файлы от правки и не получить молча сломанный useradd
Запретить правку файла даже руту
chattr +i /etc/passwd
useradd после этого выдаёт cannot open /etc/passwd и завершается с кодом 0. Пользователь не создаётся, скрипт не падает, мониторинг молчит. Снял атрибут — создался.
chattr +a /var/log/secure
error: failed to rename: Operation not permitted
rc=0
find / -xdev -perm /6000 -type f -ls 2>/dev/null
-xdev поиск по корню вернул ноль файлов и код 1 — захлебнулся на /proc. С ним нашлось 16 штук. Пустой вывод легко принять за чистую систему. Старый синтаксис +6000 GNU find уже не понимает, отвечает invalid mode.
setfacl -m u:lisa:rw file
setfacl -b file
ls -l появляется плюс: -rw-rw-r--+, после сброса исчезает — по нему и замечают ACL.
auditctl -w /etc/passwd -p wa -k pwd_change
/etc/audit/rules.d/audit.rules.
iptables -A INPUT -m conntrack \
--ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -P INPUT DROP
iptables -P INPUT DROP первой командой на удалённом сервере обрывает соединение сразу. И два уточнения: -m state до сих пор принимается, но актуален -m conntrack --ctstate, а iptables -V на 24.04 отвечает v1.8.10 (nf_tables) — правила уезжают в nftables.chattr +i на боевых конфигах ставите или считаете это ловушкой для самого себя?
Ходовой набор для отладки bash-скрипта известен всем: set -euo pipefail сверху, set -x вокруг мутного места и ловушка DEBUG с read, чтобы останавливаться перед каждой командой. Прогнал этот набор на Ubuntu 24.04, bash 5.2. Три вещи из него ведут себя не так, как ожидаешь.
Сначала то, что работает без оговорок:
# падаем на ошибке, пустой переменной и в пайпах
set -euo pipefail
pipefail не декорация: false | true без него даёт код 0, с ним 1. А set -u ловит то, до чего не доберётся проверка синтаксиса:
echo "путь: /${UNSET_VAR}/data"
# bash: UNSET_VAR: unbound variable
if — код возврата внутри перестал что-либо значить:
check() { false; echo "функция продолжила"; }
if check; then :; fi
# echo выполнился, rc=0
set -x, но дефолтный PS4 печатает голый плюс — в скрипте на 300 строк не поймёшь, где ты:
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
do_something_risky
set +x
+ g.sh:4: mkdir -p /tmp/zz
read и ловушка DEBUG:
dbg() { read -p "$BASH_SOURCE:$LINENO? " _; }
trap 'dbg' DEBUG
$LINENO и $BASH_SOURCE внутри функции указывают на саму функцию. На всех командах печаталось line=3 — строка, где стоит read. Реальные 6 и 7 не появились ни разу. Координаты надо передавать аргументами:
dbg() { read -r -p "[$1:$2] $3? " _; }
trap 'dbg "$BASH_SOURCE" "$LINENO" "$BASH_COMMAND"' DEBUG
set -T виден только вызов work, а команды в её теле пропадают — как раз там, где обычно и прячется баг.
set -T
read в ловушке читает тот же stdin, что и скрипт. Подал в цикл три строки — дошла одна:
printf 'alpha\nbeta\ngamma\n' | bash f.sh
получил: beta
read -p не печатается вовсе, и скрипт молча проносится мимо пауз. Лечится перенаправлением:
dbg() { read -r -p "$1? " _ < /dev/tty; }
/dev/null.bash -n, который советуют как «проверку перед запуском». Он про синтаксис и только: незакрытый if поймал с кодом 2, а несуществующую команду и rm -rf /$UNSET_VAR/data пропустил с кодом 0.set -T вообще используете или обходитесь set -x?
Чек-лист как проверить софт из чужой подборки
На Хабре попалась подборка «Стек российского сисадмина в 2026». Идея хорошая, а вот что с ней делать дальше — вопрос. Такие списки выходят каждый год, и половина позиций в них кочует по инерции. Автор списал у прошлогоднего автора, а проект уже год как не двигается или тихо переехал на несвободную лицензию.
Вот шесть проверок, которые снимают вопрос за десять минут. До того, как ты выкатишь это в прод.
1. Паспорт репозитория
R=zabbix/zabbix
curl -s "https://api.github.com/repos/$R" \
| jq '{pushed_at, archived, license: .license.spdx_id}'
archived: true — проект заморожен официально, дальше можно не смотреть.
P=$(curl -s "https://api.github.com/repos/$R" \
| jq -r .pushed_at)
echo $(( ($(date +%s) - $(date -d "$P" +%s)) / 86400 ))
S=$(date -u -d '90 days ago' +%Y-%m-%d)
curl -s "https://api.github.com/repos/$R/commits\
?since=$S&per_page=100" \
| jq -r '[.[] | (.author.login // "?")]
| group_by(.) | map("\(length) \(.[0])") | .[]' \
| sort -rn | head -5
.author.login // "?" конвейер молча теряет коммиты, у которых автор не привязан к аккаунту GitHub. На макете из пяти коммитов jq вернул четыре. Оператор `//` в jq применяется ко всему потоку, а не к каждому элементу — скобки обязательны.spdx_id. Значение NOASSERTION означает, что GitHub не смог сопоставить файл лицензии ни с одной стандартной. Чаще всего это BSL, SSPL или самописный текст с ограничениями на коммерческое использование. Открывай LICENSE руками.
for p in netdata zabbix-server-pgsql; do
v=$(apt-cache madison "$p" 2>/dev/null | head -1)
printf '%-22s %s\n' "$p" "${v:-НЕТ В РЕПАХ}"
done
netdata нашёлся в noble/universe, zabbix-server-pgsql — нет, ставить придётся из репозитория вендора. Отдельная засада: apt-cache madison и apt-cache policy возвращают код 0, даже когда пакета не существует, просто печатают пустоту. Конструкция apt-cache policy X || echo "нет" не сработает никогда — проверяй пустоту переменной, как выше.
curl -s -H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/rate_limit | jq .rate
🩹 Прогнал 10 популярных регулярок. Четыре врут
Салют, дежурный по проду.
Попалась подборка «10 регулярок для админа» — та самая, что кочует по всем каналам. Вместо того чтобы репостнуть, прогнал её в песочнице: Ubuntu 24.04, GNU grep 3.11, GNU sed 4.9. Четыре пункта из десяти работают не так, как обещает комментарий рядом.
Поиск IP ловит версию ядра
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' file.log
5.15.0.91 (версия ядра), 999.999.999.999 и 1.2.3.4 — откушенный кусок от 1.2.3.4.5. Октеты не проверяются, границы тоже. -w не спасает: точка не словесный символ.
grep -oP '(?<![\d.])((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\
\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?![\d.])' file.log
-P, а это PCRE, на BSD-grep не заработает.
sed -i '/^$/d' file.txt
^$ — это строка нулевой длины. Строка из трёх пробелов останется, строка с табом останется. Проверил через cat -A. Правильно так:
# сначала посмотреть, что уйдёт
sed -n '/^[[:space:]]*$/p' file.txt
# и только потом править, с бэкапом
sed -i.bak -E '/^[[:space:]]*$/d' file.txt
-i без .bak переписывает файл на месте. Один раз ошибёшься с шаблоном — восстанавливать неоткуда.
grep -Eo 'https?://[^ ]+' file.txt
см. (http://foo.org/x) и "https://bar.io/y" вернул адреса вместе со скобкой и кавычкой. А в строке с табуляцией утащил ещё и следующее слово — таб это не пробел.
grep -Eo 'https?://[^[:space:]<>"()]+[^[:space:]<>"(),.;:]' \
file.txt
.{100,} — это сто и большеLC_ALL=C считается длинной (120 байт), под UTF-8 — нет. Проверил обе, результат разный. Если нужны символы и строго больше — берите awk 'length($0) > 100'.^# не видит комментарии с отступом, -v 'ERROR' выкидывает строки со словом NOERROR, а \s в седьмом пункте — GNU-расширение, на macOS и busybox отвалится, там нужен [[:space:]].
💀 Процесс намертво завис в проде? Ставим диагноз без kill -9
Если сервис перестал отвечать, рука сама тянется к kill -9. Но перезапуск вслепую не решает проблему — он уничтожает контекст, и сбой повторится. Показываю, как заглянуть внутрь зависшего приложения на лету и поставить точный диагноз, не останавливая процесс.
Базовая диагностика зависания
# 1. Находим PID проблемного процесса по имени.
pidof my_process
# 2. Подключаемся к процессу на лету и слушаем ввод-вывод.
# -t добавит таймстампы: видно, КОГДА процесс замер.
sudo strace -tt -p <PID> -e trace=read,write
# 3. Отслеживаем ВСЕ файловые операции.
# ВАЖНО: не 'open', а группа %file — современный glibc зовёт openat(), а не open(),
# поэтому одиночный 'open' почти ничего не покажет. %file ловит open, openat, stat, access.
sudo strace -p <PID> -e trace=read,write,%file
# Если ждёшь именно сетевую блокировку (отвалившаяся БД, зависший сокет):
sudo strace -p <PID> -e trace=%net
# 4. Если вывод strace замирает на строке вида:
# read(5,
# — процесс жив, но ждёт данных в дескриптор №5.
# Отключаемся (Ctrl+C НЕ убьёт процесс) и смотрим, что это за дескриптор:
sudo lsof -p <PID> -a -d 5
# Быстрая альтернатива без lsof — прямо из /proc:
sudo ls -l /proc/<PID>/fd/5
read() из сокета из-за отвалившейся БД или недоступного пайпа, а не находиться в deadlock. Диагноз без правки кода и простоя на рестарт.
☠️ Твой ControlMaster не работает? Исправляем ошибки и ускоряем SSH
Тратишь время на авторизацию ради одной команды, а скрипты гоняешь через кривые конструкции? Многие мануалы содержат ошибки: передача команд через cat ломает кавычки, а ControlMaster=no убивает мультиплексирование. Держи 6 безопасных хаков для ускорения работы с SSH.
Выполнение команд без сессии
# Выполнить одну команду и сразу отключиться (удобно для скриптов).
ssh user@host df -h
# Безопасное выполнение списка команд из локального файла.
# Вместо ломающего кавычки cat — кормим скрипт прямо в удалённый bash:
ssh user@host "bash -s" < commands.txt
# Открыть удалённый файл в ЛОКАЛЬНОМ vim по scp. Требует плагина netrw (в комплекте vim).
# Обрати внимание на ДВА слеша: // = абсолютный путь от корня.
vim scp://user@host//etc/nginx/nginx.conf
# Сравниваем без скачивания. Дефис '-' заставляет diff читать ВТОРОЙ операнд
# из stdin (это вывод ssh). Порядок: слева локальный, справа удалённый.
ssh user@host cat /etc/nginx/nginx.conf | diff ./local_nginx.conf -
# Выполняем команду на сервере и забираем результат в локальный буфер.
ssh user@host uname -a | xclip -selection clipboard
# Ставим FUSE-драйвер файловой системы.
sudo apt-get install sshfs
# Монтируем удалённый каталог — работаем через обычные ls/cp.
# reconnect спасает сессию при обрыве связи.
sshfs user@host:/home/user ~/remote_files/ -o reconnect
# Добавь в ~/.ssh/config. Каталог для сокетов создай заранее:
# mkdir -p ~/.ssh/sockets
# ВАЖНО: ControlMaster именно 'auto', иначе переиспользование не сработает.
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%C
ControlPersist 10m
ssh, scp и sftp полетят через него мгновенно.~/.ssh/config прямо сейчас на предмет вредных настроек!