TGViewer
Channel Public Channel
LinuxSkill - Сводки с прода и Шпаргалки

LinuxSkill - Сводки с прода и Шпаргалки

@linuxskill

Следим за новостями Linux, DevOps и ИБ, чтобы быть готовым к любым факапам.
Бонусом — плотные шпаргалки и чеклисты для ежедневной работы в терминале.

📩 По всем вопросам: @chorapov

Зеркало в MAX: https://max.ru/LinuxSkill

РКН https://vk.cc/cMUwm4
Subscribers
10.7K
Photos
66
Videos
102
Links
529
Recent Posts 20 shown
Post #1240 600
📊 Диск тормозит, а iostat показывает норму? Ищем хвост задержек

Приложение подвисает на записи, в логах таймауты к БД, а iostat рисует w_await в пару миллисекунд. Подвох в том, что iostat показывает среднее: 990 запросов по 0.1 мс и 10 по 200 мс дают в среднем 2 мс. Показываю, как достать хвост, который тонет в среднем.

Что на самом деле показывает iostat

# 1. -x — расширенные колонки, раз в секунду
iostat -x 1
# r_await и w_await — СРЕДНЕЕ за интервал:
# редкие зависшие запросы в нём растворяются


ВАЖНО: %util под 100% на SSD, NVMe и RAID не значит, что диск упёрся: они обслуживают запросы параллельно. Это прямо написано в man iostat.

Гистограмма вместо среднего

# 2. В Ubuntu утилита идёт в пакете bpftrace.
# Ctrl+C — и получаешь распределение задержек.
sudo biolatency.bt

@usecs:
[16, 32) 426043
[32, 64) 15436
[64, 128) 527
...
[1K, 2K) 24
[2K, 4K) 17
[4K, 8K) 13


Проверил на тестовой нагрузке (столбики убрал, чтобы влезло в экран): iostat в тот же момент показывал w_await 0.02 мс, а в хвосте висели запросы по 4–8 мс — в сотни раз дольше среднего.

Чей I/O висит

# 3. Каждый запрос дольше 10 мс — с процессом.
# Порог подстрой: для HDD 10 мс — норма.
# На tracepoint'ах: не сломается с новым ядром.
sudo bpftrace -e '
t:block:block_bio_queue {
@ts[args->sector] = nsecs;
@who[args->sector] = comm;
}
t:block:block_rq_complete /@ts[args->sector]/ {
$ms = (nsecs - @ts[args->sector]) / 1000000;
if ($ms >= 10) {
printf("%s %d ms\n", @who[args->sector], $ms);
}
delete(@ts[args->sector]);
delete(@who[args->sector]);
}
END { clear(@ts); clear(@who); }'


Видишь kworker или jbd2 вместо своего процесса — это отложенная запись из page cache. Настоящего писателя покажет pidstat -d 1.

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

❗️ Нравится формат? Ставь 👍
👉 Рубрика: #шпаргалка@LinuxSkill
#Linux #eBPF #Storage #Troubleshooting #SysAdmin #DevOps
  • 👍 6
Post #1239 808
💀 CPU под 100%, а в top пусто? Ловим невидимые процессы через eBPF

Сервер греется, в шапке top us под 90%, а в списке процессов — никого. Классика: кто-то плодит процессы, которые живут 10–50 мс, а top опрашивает /proc раз в 3 секунды и их просто не застаёт. Показываю, как поймать виновника, не роняя прод.

Ловим каждый запуск

# 1. В Ubuntu утилита идёт в пакете bpftrace
sudo apt install bpftrace

# Каждый execve() в реальном времени:
# время, PID и PPID родителя.
sudo execsnoop.bt

TIME            PID   PPID  ARGS
16:55:42.684175 1112 1097 /bin/true
16:55:42.685129 1113 1097 curl --version
16:55:42.691294 1114 1097 sleep 0.05


Один и тот же PPID и новые процессы каждые 50 мс — вот он, виновник.


Кто плодит больше всех

# 2. Считаем запуски по родителю за 10 секунд.
# Подсчёт идёт ПРЯМО в ядре, без потока событий.
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
{ @[comm, curtask->real_parent->tgid] = count(); }
interval:s:10 { exit(); }'

# Вывод: @[bash, 1097]: 525
# bash с PID 1097 за 10 секунд породил 525 процессов.

# 3. Смотрим, кто это и откуда он взялся
pstree -sp \<PPID>
ps -o pid,etime,cmd -p \<PPID>



Почему не strace -f
Проверил: dd с bs=1 под strace работает в ~47 раз медленнее. И в ~37 раз — даже если трассировать только accept(), которого dd вообще не вызывает: ptrace останавливает процесс на КАЖДОМ системном вызове. eBPF процесс не останавливает и считает события в ядре.


ВАЖНО: bcc-версия execsnoop-bpfcc компилирует программу на лету и без linux-headers-$(uname -r) падает с «Unable to find kernel headers». execsnoop.bt работает через BTF, заголовки ему не нужны. Есть ли BTF в ядре: ls /sys/kernel/btf/vmlinux.


Вместо «CPU горит, а кто — непонятно» получаешь имя процесса, аргументы и PID родителя. Дальше дело техники: поправить крон или скрипт.


❗️ Нравится формат? Ставь 👍
👉 Рубрика: #шпаргалка@LinuxSkill
#Linux #eBPF #bpftrace #Troubleshooting #SysAdmin #DevOps
  • 🔥 4
  • 👍 3
Post #1237 1.06K
🔥 Знаешь только ip a? 10 неочевидных команд ip для Linux

Большинство админов знают про ip ровно две команды — ip a и ip r, а дальше ifconfig по старой памяти. Держи 10 команд, которые покажут то, чего не видно в ip a: скрытые маршруты VPN, дропы на интерфейсе и мёртвый шлюз. Проверил на iproute2 6.1.

1️⃣ Вся сеть одним экраном, в цвете:

ip -br -c a

Интерфейс, состояние и адреса — по строке на каждый. -br работает и с link.

2️⃣ Куда реально уйдёт пакет:

ip route get 8.8.8.8

# какая запись в таблице сработала
ip route get fibmatch 8.8.8.8

Покажет интерфейс, шлюз и source-адрес. Незаменимо, когда маршрутов много и непонятно, какой победил.

3️⃣ Маршруты, которых нет в ip route:

ip rule
ip route show table all

ВАЖНО: ip route показывает только таблицу main. wg-quick с AllowedIPs = 0.0.0.0/0 уводит дефолт в таблицу 51820, а ip route делает вид, что всё как раньше.

4️⃣ Сменить шлюз без риска потерять сервер:

sudo ip route replace default via <gw>

Атомарно: нет окна без маршрута, как между del и add. Упадёт с ошибкой — старый маршрут останется на месте.

5️⃣ Дропы и ошибки на интерфейсе:

ip -s link show <iface>

# двойной -s — ошибки с разбивкой: crc, frame, fifo
ip -s -s link show <iface>

Растут errors или dropped — копай в кабель, драйвер или буферы, а не в приложение.

6️⃣ Жив ли шлюз на L2:

ip neigh show dev <iface>

REACHABLE и STALE — норма. Шлюз висит в FAILED или INCOMPLETE — он не отвечает на ARP, и пинговать дальше бесполезно.

7️⃣ Сеть в прямом эфире:

ip -ts monitor link route

Каждое изменение с меткой времени: упал линк, DHCP или VPN переписали маршрут. Оставь в tmux и лови, кто ломает сеть по ночам.

8️⃣ Отрезать IP без фаервола:

sudo ip route add blackhole 203.0.113.5/32

# проверить и убрать
ip route show type blackhole
sudo ip route del blackhole 203.0.113.5/32

Ответы на этот адрес уходят в никуда, TCP-сессия с ним не установится. ip route get по нему вернёт Invalid argument — так ядро отвечает на blackhole, это не ошибка.

9️⃣ IP интерфейса в скрипте без awk-акробатики:

ip -j -4 addr show dev <iface> \
| jq -r '.[0].addr_info[0].local'

-j отдаёт JSON: парсинг не сломается, когда поменяется формат вывода. Нужен пакет jq.

🔟 Процесс без сети за две команды:

sudo ip netns add lab
sudo ip netns exec lab <command>

# убрать за собой
sudo ip netns del lab

В новом namespace только выключенный lo. Удобно проверить, как приложение переживёт падение сети, не трогая хост.

Всё, что меняешь через ip, живёт до перезагрузки. Постоянные настройки — в netplan, NetworkManager или systemd-networkd.

А какую из десяти ты уже держишь в алиасах?

❗️ Нравится формат? Ставь 👍

👉 Рубрика: #шпаргалка@LinuxSkill

#Linux #Networking #iproute2 #CheatSheet #SysAdmin #DevOps
  • 👍 14
  • 🔥 2
Post #1236 1.13K
Разобрать строку штатным <regex.h> и не получить молчаливое «нет совпадения».

Проверял на glibc, gcc 13.3, Ubuntu 24.04. Все фрагменты ниже собраны в одну программу и скомпилированы с -Wall -Wextra без единого предупреждения.


Скомпилировать шаблон

regex_t re;
int rc = regcomp(&re, "^([a-z]+)=([0-9]+)$",
REG_EXTENDED);


REG_EXTENDED включает ERE: скобки и + работают без обратных слешей. Без флага получите BRE, где группа пишется как \(...\).


Понять, почему не скомпилировалось

char err[256];
if (rc != 0) {
regerror(rc, &re, err, sizeof err);
fprintf(stderr, "regcomp: %s\n", err);
}


Буфер обязан быть массивом. На незакрытой скобке [a-z получите Unmatched [, [^, [:, [., or [=. Нюанс: regfree вызывайте только после успешного regcomp, для неудачной компиляции освобождать нечего.


Сопоставить и достать группы

regmatch_t m[3];
const char *s = "app=100";
if (regexec(&re, s, 3, m, 0) == 0)
printf("%.*s\n", (int)(m[1].rm_eo - m[1].rm_so),
s + m[1].rm_so);


m[0] — всё совпадение, m[1] и дальше — группы по порядку. На app=100 вышло app в первой группе и 100 во второй. Нюанс: смещения в байтах, не в символах. В строке ключ=42 вторая группа начинается с байта 9, хотя символов перед ней пять. И если компилировали с REG_NOSUB, массив m не заполняется вовсе.


Не забыть освободить

regfree(&re);


Замерил: 100 тысяч компиляций в цикле без regfree раздули процесс до 1.4 ГБ, с ним — 1.8 МБ. В демоне, который компилирует шаблон на каждый запрос, это вопрос часов.


Где POSIX молчит вместо ошибки

Самое неприятное не в том, чего API не умеет, а в том, как он об этом сообщает:

\d+     на "abc 123" -> No match
<.*?> на "<a><b>" -> <a><b>
(?<=x)y -> Invalid preceding regular expression


Просмотры и именованные группы падают на компиляции — это честно. А вот \d компилируется и тихо ничего не находит, вместо него нужен [0-9]. И .*? не ошибка, а просто жадный .*: вернул всю строку вместо <a>. Привычки из PCRE здесь ломают логику молча.

А вы в C-утилитах держитесь <regex.h> или сразу тянете PCRE2?

#Linux #C #POSIX #regex #DevOps
Post #1235 1.16K
Закрыть периметр правилами 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.

Ограничить SSH подсетью и числом сессий


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 — в конфигурацию сети, иначе следующий ребут откроет всё обратно.

А вы вредоносные IP баните iptables напрямую или отдаёте это CrowdSec и fail2ban?

#Linux #Security #iptables #Networking #DevOps
  • 👍 4
Post #1233 1.07K
Добавить пользователя в группу и не выкинуть его при этом из 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, трогает ровно одну группу.

Сменить GID существующей группы


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.

А вы группы раскатываете Ansible-модулями или скриптом с groupadd -f?

#Linux #Security #Ansible #DevOps #Bash

---

Что нашлось при проверке

Главную ловушку воспроизвёл. Завёл lisa в sudo, developers, ops. После usermod -G developers lisa осталась одна developers, код возврата 0, ни слова в выводе. Исходник называет это в «честном минусе», но в самом пункте 7 стоит только скобка «обязательно -a» — я вынес разбор в тело, потому что это главное содержание всей темы.

Находка сверх исходника: `groupmod -g` оставляет файлы без группы. Сменил GID у developers с 1004 на 2000 — каталог /data стал показывать 1004 вместо имени. Формально данные не потеряны, но группа к ним больше не имеет отношения. Исходник даёт команду без единой оговорки. Добавил find -nogroup и chgrp -R как лечение.

`groupadd -f` для идемпотентности. Обычный вызов на существующей группе — код 9. В скриптах инициализации, о которых говорит сам исходник, это ровно та причина, по которой люди начинают городить проверки через grep /etc/group. Флага -f в исходнике нет.

`groupdel` на основной группе. cannot remove the primary group of user 'bob', код 8. Проверил обе ветки — вторичная удаляется молча.

`groupadd -r` даёт GID из системного диапазона. У меня 996 против 2002 у обычной. Исходник пишет «GID до 1000» — подтвердилось.

Из пятнадцати пунктов оставил шесть. Убрал groups, id, chgrp, find -group, newgrp, groupmod -n, gpasswd -A как отдельные блоки — по ним нечего добавить сверх справки. Мысль про groups против id вплёл в текст: она объясняет, почему пользователь «не видит» новую группу.

Метрики: объём 2027, подводка 67 символов, форма — инфинитив цели. Строк кода длиннее 60 нет, восклицательных нет, хештегов 5.
  • 👍 3
Post #1232 1.24K
Собрать архив с исключениями и не обнаружить потом, что исключения не сработали.

Исключить каталог из архива


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


Нюанс: при упаковке по абсолютному пути tar срезает ведущий слеш и пишет 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. Чтобы поправить сжатый архив, его придётся распаковать целиком.

Сжать быстрее и плотнее gzip


tar --zstd -cf arch.tar.zst data


На моих тестовых данных zstd дал 199 байт против 225 у gzip. Нюанс: если пакета 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 по каждому расхождению. Удобно проверить бэкап, не распаковывая.

А вы чем сжимаете бэкапы — gzip по привычке или уже переехали на zstd?

#Linux #tar #Backup #Bash #DevOps
  • 👍 7
Post #1230 1.18K
Собрать дерево каталогов одной строкой и не получить папку с фигурными скобками в имени.

Создать вложенную структуру


mkdir -p project/src/{configs,utils,tests}


Нюанс: раскрытие скобок — фича bash, а не mkdir. В 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 -?

#Linux #Bash #Terminal #Scripting #DevOps
  • 👍 6
Post #1229 1.27K
Не оставить пароль в 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 #Bash #Terminal #Security #DevOps
  • 👍 4
Post #1228 1.37K
Смонтировать сетевую шару и не подвесить процессы, когда сервер отвалится.

Убрать пароль из 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


Проверил на tmpfs: после -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= отцепит шару после простоя — на нестабильном канале это лучше, чем висящая точка.

Не залипнуть на медленном NFS


mount -t nfs -o soft,timeo=50,retrans=2 \
10.0.0.10:/data /mnt/nfs


timeo в децисекундах, так что 50 — это 5 секунд. По умолчанию для TCP стоит 600, то есть минута, да ещё с линейным ростом после каждой переотправки.

Нюанс: man про soft пишет прямо — silent data corruption. Приложение получит I/O error, и если оно его не обработает, запись потеряется тихо. Для баз и любой записи оставляйте hard. И не тащите из старых шпаргалок intr — опция игнорируется с ядра 2.6.25.

А вы сетевые шары монтируете через fstab или отдаёте systemd с automount?

#Linux #NFS #Samba #systemd #DevOps
  • 👍 5
Post #1226 1.44K
Найти на сервере скрытых рутов и не поверить проверке, которая их не видит.

Найти лишние учётки с нулевым 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.

Убедиться, что root не пускают по SSH


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 печатает итоговую конфигурацию после всех включений.

Проверить sudoers перед сохранением


visudo -c -f /etc/sudoers.d/devops


visudo без аргументов знают все, а вот -c -f работает с любым файлом и годится для CI или проверки после Ansible. На кривом правиле показывает строку и место ошибки, на нормальном — parsed OK. Заодно sudo -l покажет, что реально разрешено текущему пользователю.

Найти SUID-бинарники


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 и десяток других утилит — у всех есть побег в шелл. Разрешать по одной команде безопаснее, чем кажется, только если это не интерпретатор.

А вы root на серверах отключаете совсем или держите для аварийной консоли?

#Linux #Security #SSH #sudo #DevOps
  • 👍 5
Post #1225 1.57K
Выкинуть из скрипта форки 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 мс. Разница в двести раз, и это не про скорость самой утилиты, а про форк процесса на каждый вызов.

Собрать файлы в массив вместо ls


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 даёт то же время с микросекундами и без единого процесса.

А что вы чаще выносите из скриптов — cut и awk или всё-таки оставляете, потому что читается понятнее?

#Linux #Bash #Shell #Scripting #DevOps
  • 👍 4
  • 🔥 1
Post #1224 1.72K
Обойти массив путей с пробелами и не получить вместо одной папки три несуществующих.

Перебрать элементы, не разорвав их по пробелам


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.

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

А вы конфиги в bash разбираете ассоциативными массивами или сразу отдаёте jq?

#Linux #Bash #Shell #Scripting #DevOps
  • 👍 2
Post #1222 1.44K
Прокинуть скрипт и каталог на сервер и не собрать по дороге чужие абсолютные пути
Всё проверено на 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"


Управление вернулось через 181 мс, при этом клиентский процесс продолжает висеть, пока команда не отработает. Удобно для проброса портов: ssh -f -N -L 9998:localhost:15672 user@host.

Держать ключ в агенте ограниченное время


ssh-add -t 30 ~/.ssh/id_ed25519


Через 30 секунд ключ уйдёт из памяти агента сам. Нюанс: без запущенного ssh-agent команда просто скажет Could not open a connection to your authentication agent.

Про sshpass из подборок: он кладёт пароль в аргументы процесса, где его видно в ps любому пользователю системы. Для закрытого стенда — ладно, дальше стенда лучше ProxyJump и ключи.

А чем пробрасываете доступ к внутренним панелям — -L руками или прописываете в конфиг?

#Linux #SSH #Networking #DevOps #Terminal
  • 👍 7
Post #1221 1.66K
Сервис упал ночью, до утра гигабайт логов. Порядок, в котором в них лезть

Начать с ошибок текущей загрузки


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, и ошибки диска, и отвалившийся сетевой интерфейс.

Проверить блокировки SELinux


ausearch -m avc -ts recent


Нюанс: на Debian и Ubuntu команда чаще всего вернёт пустоту — там AppArmor, а не SELinux, и /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 такой вариант нашёл два. Разбор по кавычкам берёт код ответа там, где он реально лежит.

Для пары серверов этого хватает. Когда машин десятки, CLI перестаёт работать и нужен Loki или ELK — но и там первый вопрос будет тот же: какое окно и какой приоритет.

А с чего начинаете вы, когда прод упал и надо быстро локализовать причину?

#Linux #Logs #journalctl #Troubleshooting #DevOps
  • 👍 5
  • 🔥 1
Post #1220 1.72K
Диски гостей раздулись, а внутри места полно. Вернуть его хосту — и не просадить запись

Проверить, поддерживает ли диск 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.

Настроить регулярный TRIM


systemctl enable --now fstrim.timer
systemctl list-timers fstrim.timer


Нюанс: свой скрипт в /etc/cron.weekly писать не нужно — в пакете util-linux уже лежит готовый fstrim.timer с OnCalendar=weekly и разбросом запуска до 100 минут, чтобы виртуалки не пошли в discard одновременно.

Если всё-таки делаешь через cron, помни про две вещи. Скрипт без строки #!/bin/sh не выполнится: run-parts: failed to exec: Exec format error. И файл с точкой в имени, вроде fstrim.sh, run-parts молча пропустит.

Не включать онлайн-TRIM в fstab


UUID=xxxx / ext4 defaults,discard 0 1


Опция discard при монтировании шлёт запрос на очистку при удалении каждого файла. Пакетный TRIM раз в неделю делает то же самое, но одним заходом и в спокойное время.

Всё это работает, только если discard включён в настройках диска на самом гипервизоре. Иначе гость честно шлёт команды, а хост их игнорирует и продолжает копить мусор.

А вы fstrim по таймеру гоняете или руками, когда место кончилось?

#Linux #Virtualization #Proxmox #fstrim #DevOps
  • 🔥 2
Post #1218 1.69K
Как достать пароль из 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


Отдаёт в stdout все записи вместе с паролями открытым текстом. Полезно при переезде, но перенаправлять это в файл на общей машине — плохая идея.

Про clip, который советуют для копирования пароля в буфер. Именно в сценарии «GUI лёг» он и не работает: без графической сессии команда отвечает All clipping programs failed. Tried xclip. Смысл в нём есть только в живой графике, где он же и чистит буфер через 10 секунд.

И про совет закрыть GUI перед работой в консоли. Проверил: при открытой базе KeePassXC создаёт рядом файл .kdbx.lock. Утилита о нём знает, так что «гарантированного повреждения» не будет — но менять базу из двух мест одновременно всё равно не стоит, последняя запись затрёт чужие правки.

А вы держите пароли в локальных kdbx или переехали на self-hosted Vaultwarden?

#Linux #KeePass #Security #CLI #DevOps
  • 👍 2
Post #1217 1.87K
Если гоняешь ansible по сотне серверов, SSH каждый раз делает хендшейк заново

Переиспользовать одно TCP-соединение


# ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%C
ControlPersist 10m


Замерил на локальном sshd: первое подключение 0.22 секунды, второе и третье — по 0.01. Разница в двадцать раз, и это на loopback, где сети как таковой нет. На реальном канале с задержкой выигрыш будет больше.

Каталог ~/.ssh/sockets надо создать заранее, сам он не появится.

Не упереться в лимит длины пути

Нюанс: %C тут не для красоты. Классический %r@%h:%p на длинных именах хостов выходит за предел, и ssh отказывается работать:


ControlPath too long ('/root/.ssh/sockets/xxx...
-root@127.0.0.1:2222' >= 108 bytes)


Проверил на пути в 109 байт — ровно на границе. %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, но сначала надо догадаться, что дело в нём.

А вы ControlPersist держите глобально на Host * или включаете точечно?

#Linux #SSH #Ansible #DevOps #Terminal
  • 👍 4
  • 🔥 1
Post #1216 1.89K
Пробросить 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


Отправляет ровно один фрейм из stdin и закрывает сессию. Удобно дёргать из скрипта. Локальный эхо-сервер вернул get_metrics и завершился с кодом 0.

Пробить туннель на тестовый сервер с самоподписанным TLS


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 вручную.

А чем вы пробиваете туннель, когда наружу открыт только 443?

#Linux #websocat #Networking #Tunneling #DevOps
  • 👍 2
  • 👀 2
  • 🔥 1
Post #1214 1.57K
Закрыть системные файлы от правки и не получить молча сломанный useradd

Запретить правку файла даже руту


chattr +i /etc/passwd


Нюанс: useradd после этого выдаёт cannot open /etc/passwd и завершается с кодом 0. Пользователь не создаётся, скрипт не падает, мониторинг молчит. Снял атрибут — создался.

Разрешить логу только дозапись


chattr +a /var/log/secure


Нюанс: ротация делается переименованием, а его атрибут не пускает. Запустил logrotate на таком файле:


error: failed to rename: Operation not permitted
rc=0


Снова ноль на выходе. Файл не ротировался, cron считает, что всё прошло, логи растут до конца места.

Найти файлы с SUID и SGID


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


Нюанс: правило живёт до перезапуска auditd. Постоянные пишутся в /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 на боевых конфигах ставите или считаете это ловушкой для самого себя?

#Linux #Security #Hardening #iptables #DevOps
  • 👍 5
  • 🔥 1
Older posts →

About this channel

How can I read @linuxskill without a Telegram account?
TGViewer shows the public web preview Telegram publishes for LinuxSkill - Сводки с прода и Шпаргалки: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does LinuxSkill - Сводки с прода и Шпаргалки have?
LinuxSkill - Сводки с прода и Шпаргалки (@linuxskill) has 10.7K subscribers on Telegram, refreshed roughly every 30 minutes.
Does LinuxSkill - Сводки с прода и Шпаргалки know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →