tgindex
Остальные 90%

Остальные 90%

Статистика
@final90percentрусский

Не так страшны первые 90% проекта, как оставшиеся 90%. Заметки о неожиданных трудностях в Linux и их героическом преодолении. Автор: @korneev_es ВК: https://vk.com/final90percent

Последний пост
29 дек.
Последнее чтение
13 авг.
Постов за неделю
0
Всего постов
20
Тип
открытый
Язык
русский
В каталоге с
13 авг.
Подписчики
724
0 за 2 дн.
Сутки
0
0,00%
Неделя
 
Месяц
 
Просмотров на пост
778
20 постов
Вовлечённость
107,5%
к подписчикам
Постов в день
0,0
всего 20
Упоминаний
0
каналов
Охват размещения
оценка
1/24сутки в ленте
1/48двое суток
1/72трое суток

Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.

Посты

  • 29 дек.7852816

    Заметка дня #11 Изредка, пробуя установить разные версии одной и той же утилиты, можно наткнуться на неожиданное поведение: несмотря на то, что мы только что установили вторую версию приложения, Bash упорно отказывается её видеть. В новом же терминале, однако, всё работает корректно, и это может выглядеть подозрительно. Оказывается, Bash ищет утилиту лишь раз, при первом её вызове - после этого он хранит путь к найденному исполняемому файлу во внутреннем ассоциативном массиве, и все последующие вызовы команды сводятся к вызову файла, расположенному по сохранённому ранее пути. Именно поэтому, если мы вызвали версию утилиты, а затем удалили её, то без явных подсказок Bash новую версию искать не станет. Посмотрим на поведение Bash на примере: создадим файл do-not-delete-me.sh по пути /usr/local/bin и убедимся, что всё работает: # Create a file: $ cat do-not-delete-me.sh #!/bin/bash echo 'Hello, Elliot' # Make it runnable and move to /usr/local/bin: $ chmod +x do-not-delete-me.sh $ sudo mv do-not-delete-me.sh /usr/local/bin # Call the binary! $ do-not-delete-me.sh Hello, Elliot Теперь переместим скрипт в ~/.local/bin, тоже содержащийся в переменной $PATH - технически, Bash по-прежнему должен найти команду, однако этого не происходит: $ sudo mv /usr/local/bin/do-not-delete-me.sh ~/.local/bin $ do-not-delete-me.sh bash: /usr/local/bin/do-not-delete-me.sh: No such file or directory Несложно дополнительно убедиться, что открытый в отдельном окне терминал вызвать команду всё-таки может. Исправить ситуацию можно, используя встроенную команду Bash hash, отвечающую за построение ассоциативного массива, речь о котором шла выше. Вызванная без аргументов, она покажет, какие команды были запомнены: $ hash hits command 4 /usr/local/bin/do-not-delete-me.sh 1 /usr/bin/chmod 3 /usr/bin/ls ... Для удаления конкретной записи можно использовать ключ -d, однако самым коротким путём будет очистить всю таблицу одним махом: $ hash -r Теперь путь до скрипта найдётся корректный, и запуск команды завершится успехом: $ do-not-delete-me.sh Hello, Elliot $ hash hits command 1 /home/mint/.local/bin/do-not-delete-me.sh Наткнуться на эту проблему можно, перемежая установку сборок утилиты из репозиториев дистрибутива и GitHub - тот же сканер Grype служит наглядным примером. На момент написания заметки Ubuntu 22 позволяет установить версию 0.99.1, в то время как самый свежий релиз - уже 0.104.3. Мы могли установить сканер из репозиториев, обнаружить старую версию, удалить её и установить новую в другое место - и получить именно эту ошибку. Проблема стреляет крайне редко, и обычно не причиняет существенных неприятностей, однако помнить о её истоках полезно. Если мы работали удалённо по SSH, и подключение требует ввода пароля, то открывать новое соединение лишний раз обычно не хочется. #tip

  • 22 дек.8552216

    О том, что поле CN в TLS-сертификатах помечено устаревшим ещё в 2000 году, я читал не раз. И всё-таки неожиданным открытием для меня оказалось, что HTTPS-клиенты, написанные на Go 1.17+, отказываются работать с такими сертификатами совершенно. Открытием это стало, поскольку тот же curl, с которым обычно приходится иметь дело, ведёт себя иначе. Чтобы в этом убедиться, создадим ключ и сертификат, выписав его на имя localhost, и запустим с ними простой веб-сервер: # Creates files cert.pem and key.pem $ openssl req -x509 -keyout key.pem -out cert.pem \ -days 365 -nodes -subj '/CN=localhost/' # Check there is no SAN in certificate $ openssl x509 -in cert.pem -noout -ext subjectAltName No extensions in certificate $ openssl s_server -key key.pem -cert cert.pem -WWW -port 4433 Веб-сервер будет отдавать файлы из рабочей директории, так что для тестирования запросим любой из имеющихся - например, cert.pem. В соседнем терминале подключимся к серверу при помощи curl. Поскольку сертификат, который отдаёт сервер, самоподписанный, используем его же для валидации подключения: $ curl https://localhost:4433/cert.pem --cacert cert.pem -----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUO6d0vnNmNST77... ... Как мы видим, сертификат вполне удовлетворил curl. Теперь попробуем обратиться к тому же серверу, но по другому имени: # Using '--resolve' to avoid messing with /etc/hosts $ curl https://example.com:4433/cert.pem --cacert cert.pem \ --resolve 'example.com:4433:127.0.0.1' curl: (60) SSL: certificate subject name 'localhost' does not match target host name 'example.com' Как и ожидалось, теперь сертификат не принимается - ничего необычного. Возьмём теперь HTTPS-клиента, написанного на Go, который будет подключаться к тому же серверу. Убедимся, что версия go не менее 1.17, и запустим клиент: $ go version go version go1.22.1 linux/amd64 $ CERT=cert.pem URL='https://localhost:4433/' go run main.go 2025/12/22 00:33:23 Error making request: Get "https://localhost:4433/": tls: failed to verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead exit status 1 Несмотря на соответствие доменного имени и сертификата клиент отказался подключаться к серверу. В версиях go до 1.16 включительно это поведение можно было переопределить, указав переменную GODEBUG=x509ignoreCN=0, однако уже с версии 1.17 изменить поведение стандартной библиотеки нельзя. Единственный способ заставить клиента подключиться - выписать сертификат, содержащий соответствующее поле SAN. Перевыпишем сертификат crt.pem с ключом -addext и перезапустим сервер: $ openssl req -x509 -keyout key.pem -out cert.pem \ -days 365 -nodes -subj '/CN=localhost/' \ -addext 'subjectAltName = DNS:localhost' # Check SAN exists now $ openssl x509 -in cert.pem -noout -ext subjectAltName X509v3 Subject Alternative Name: DNS:localhost $ openssl s_server -key key.pem -cert cert.pem -WWW -port 4433 Теперь клиент сможет подключиться и также загрузить файл: $ CERT=cert.pem URL='https://localhost:4433/' go run main.go -----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUO6d0vnNmNST77... ... Оставляя за скобками недовольство по поводу такого неоднозначного изменения в Go (проблему решали день), выводы хочется сделать следующие: 1. необходимо внимательно следить за содержимым сертификатов - расширения уже давно стали неотъемлемой частью сертификата; 2. openssl по умолчанию выписывает сертификат, который не принимается многими клиентами - важно помнить о ключе -addext. Кстати, mkcert, имеющийся в репозиториях Ubuntu, создаст ту же пару файлов при при помощи короткой команды mkcert localhost, однако сразу с расширением SAN и не требуя запоминать десяток неочевидных ключей. В этом вопросе он однозначно обходит openssl :)

  • Привет! Последние полтора месяца в канале было тихо - пришло время нарушить молчание и поделиться, как же так вышло и чего ждать дальше. Причина затишья банальна - всё это время я помогал составлять курс, посвящённый теме SRE. На это уходило всё свободное время и все остающиеся к вечеру силы, так что в результате оформить заметки должного качества уже никак не удавалось. Ближайший месяц обещает быть таким же насыщенным, а затем, как только вернётся возможность, в канале снова начнут появляться заметки, список заготовок для которых всё это время медленно пополнялся. Чтобы заметка не выбивалась из общего потока и несла прикладной интерес, поделюсь ссылкой на один из моих любимых сборников страшилок UNIX-админов: https://www-uxsup.csx.cam.ac.uk/misc/horror.txt. Если ссылка не открывается, то есть версия на Web Archive - очень советую познакомиться с рассказами. Чрезвычайно любопытно сравнить задачи тридцатилетней давности с нынешними. Спойлер: не берусь утверждать, что 30 лет назад всё было проще. Мою любимую историю быстрее всего найти по строке From alt.folklore.computers Fri Nov 9 11:16:43 1990 - думается мне, подобные решения проблем едва ли удастся встретить в современном IT :)

  • видео или голосовое, без подписи

  • Прежде чем углубиться в изучение внутренностей Linux, мне показалось верным сделать шаг назад и обратиться к истокам - к третьему изданию Operating Systems Design and Implementation Таненбаума. В ближайшем году труду исполняется 20 лет, и столь почтенный возраст уже начинает сказываться на актуальности материала - особенно если говорить о практической его части. Взявшись за книгу, я ожидал, что мне удастся проштудировать исходный код ОС в сопровождении обширных комментариев автора, поэкспериментировать с исходниками, пересобрать систему, но трудности всплыли на первом же шагу. Последняя версия Minix, 3.3.0 на текущий момент, уже далеко ушла от описанной в книге версии 3.1.0, и скорее представляет из себя сборку NetBSD с вкраплениями #if defined(__minix). Проект сильно изменил свою структуру, и сопоставить функции и файлы, о которых идёт речь в тексте учебника, с имеющимися в проекте, стало слишком сложно. Более того, собрать проект без доработок не выйдет - сначала надо исправить несколько ошибок в исходниках. В то же время вернуться к версии 3.1.0, описываемой в книге, ещё сложнее: Makefile ожидает, что исходники лежат по определённому пути, префиксы местами захардкожены, а python так и вовсе используется версии 1.5. Всё починить и завести, конечно, можно, но задача кажется объёмной и даже перевешивающей цель повозиться с самим кодом - в общем, от этой части плана пришлось отказаться. Книгу я изучал, просто читая исходный код. К качеству этого кода тоже есть вопросы, хоть и спорить с автором я желания не имею :) И всё же сбитые отступы, использование стандарта K&R, нарушения единого стиля и прочие мелочи заставляли мозг напрягаться лишний раз. Самой крупной проблемой оказался ошибочный комментарий в ассемблерном коде (использующим свой особый диалект, к слову), из-за которого пришлось несколько дней сверяться с OsDev Wiki: сегменты описаны как flat, значит, смещение сегмента должно быть равно нулю - а оно вычисляется. В конце-концов, большие надежды я возлагал на раздел, посвящённый управлению памятью, однако MINIX 3.1.0 не реализует виртуальную память. Описанный алгоритм ощутимо проще используемого в современном Linux и мне был уже не особо интересен. И всё же учебник замечателен - выше я сказал обо всех несоответствиях ожиданиям и недостатках, которые помешали мне вынести максимум из книги, и все они уместились в одну заметку в сравнении с текстом самого труда, занимающего более 600 страниц. В книге Таненбаум говорит не только о коде, он описывает и общую теорию, рассказывает про возможные алгоритмы. Пару слов об этом я даже оставлю в комментарии к заметке. Наконец, пара слов об актуальности MINIX. Да, эта ОС уже имеет мало общего с Linux, используемые при её построении подходы (например, "безопасность важнее производительности") могли не пройти проверку временем, и всё-таки MINIX имеет огромную академическую ценность и даёт базу для дальнейшего изучения Linux. Более того, оказывается, модуль Intel ME использует MINIX 3 в качестве прошивки!

  • Клиент-серверная архитектура Docker позволяет обращаться к демону dockerd не только с локального хоста, но и с удалённого. В привычном режиме демон открывает UNIX-сокет, в "удалённом" - обычный сетевой сокет, которым используется Docker Swarm, например. Вопрос авторизации в этой схеме решается при помощи mTLS, что нравится не всем: если пользователей, обращающихся к демону, много, то управление доступами потребует выписывать и отзывать сертификаты, а выполнение всех действий общим демоном затрудняет их аудирование - все операции на хосте будут происходить от имени единого процесса dockerd независимо от того, кто их инициировал. Оказывается, первую проблему Docker всё-таки решает: хоть и не без греха, Docker-клиент умеет подключаться к серверу по протоколу SSH и обращаться к демону уже через локальный сокет - странно, что соответствующая страница документации не попадалась на глаза ранее. Заставить docker подключаться по SSH можно по-разному: используя ключ -H, настроив context или выставив переменную окружения DOCKER_HOST, которую будем использовать в примерах далее. Чтобы удалённое подключение работало, надо удовлетворить двум условиям: 1. пользователь может обращаться к сокету dockerd; 2. на хост добавлен ключ авторизации. Если первое требование ещё как-то упоминается в документации, то о втором упоминаний я не нашёл, а об ошибках docker сообщит не слишком явно. Положим, авторизация на сервере настроена не по SSH-ключу, а по паролю: DOCKER_HOST='ssh://mint@localhost' docker ps error during connect: Get "http://docker.example.com/v1.50/containers/json": command [ssh -o ConnectTimeout=30 -T -l mint -- localhost docker system dial-stdio] has exited with exit status 255, ... Permission denied, please try again. mint@localhost: Permission denied (publickey,password). В тексте ошибки docker раскрывает, что подключается к серверу, просто вызывая команду ssh с ключом -T - то есть, не аллоцируя терминал. Указать пароль в URL, судя по коду, тоже нельзя. А ещё команда раскрывает секретную опцию system dial-stdio - просто интересный факт :) Невыполнение второго условия влечёт другую неочевидную ошибку: $ DOCKER_HOST='ssh://mint@localhost' docker ps Cannot connect to the Docker daemon at http://docker.example.com. Is the docker daemon running? Здесь docker пытается обратиться к UNIX-сокету dockerd на удалённом хосте и ему это не удаётся: непривилигированный процесс пользователя не отличает отсутствие сервера от нехватки прав: # Manual check on a remote host: $ curl --unix-socket /var/run/docker.sock 'http://docker.example.com/images/json' curl: (7) Failed to connect to docker.example.com port 80 after 0 ms: Couldn't connect to server Добавив пользователя в группу docker, чтобы вызывать sudo не требовалось (что равносильно выдаче root-прав пользователю...), мы наконец доберёмся до хоста: $ DOCKER_HOST='ssh://mint@localhost' docker ps CONTAINER ID IMAGE ... Вся эта возня пригодится, если надо таскать образы в закрытый контур через хост-бастион. Находясь на хосте, с которого осуществляется прыжок в закрытый контур, образы можно передавать парой команд: $ docker pull python:3.13 ... $ docker save python:3.13 -o python.tar $ docker -H 'ssh://user:next-host' docker load -i python.tar Здесь нам не приходится передавать образ по scp, заходить на удалённый хост для его загрузки и удалять переданный образ с ФС. Экономия на спичках, но всё равно приятно :)

  • Читать и править файлы /etc/passwd или /etc/hosts приходилось, наверное, каждому инженеру Linux. В статьях, посвящённых DNS, нередко всплывает файл /etc/nsswitch.conf, задающий порядок обращений к источникам DNS-имён. Редко при этом внимание уделяется связям между этими файлами, и в целом самой системе разрешения имён NSS, частью которой они являются. NSS (Name Service Switch) - это механизм, позволяющий централизованно управлять доступными базами имён. Источников имён может быть много: в случае с доменными именами, например, это может быть файл /etc/hosts, сервис mDNS, "обычный" DNS, и порядок обращений к ним может меняться. Вместо того, чтобы каждый процесс самостоятельно решал, куда обращаться в первую очередь, POSIX предлагает использовать набор стандартных функций, просто позволяющих запросить ответ у операционной системы. Обработка ошибок сервисов и порядок обращений к ним ложится на плечи системных библиотек, поведение которых, в свою очередь, описывается в том самом файле /etc/nsswitch.conf. Что интересно, сервис не ограничивается работой с доменными именами. Помимо них он знает о назначении стандартных сетевых портов, которые обычно слушают популярные сервисы, созданных пользователях, группах пользователей и некоторых других базах, с которыми сталкиваться толком и не приходится. Для обращения к ним из CLI есть команда getent: # IPv4 of google.com? $ getent ahostsv4 google.com 142.250.9.138 STREAM google.com ... # IPv6 of google.com? $ getent ahostsv6 google.com 2607:f8b0:4002:c11::65 STREAM google.com ... # User entries for root and mint? $ getent passwd root mint root:x:0:0:root:/root:/bin/bash mint:x:1000:1000:mint:/home/mint:/bin/bash # Default ports? $ getent services ssh ntp mail ssh 22/tcp ntp 123/udp smtp 25/tcp mail В документации getent перечислены доступные базы и указано, какие системные вызовы используются для обращения к ним. К описаниям файлов, задействуемых системными библиотеками в каждом случае, можно выйти по ссылкам в документации к соответствующим системным вызовам. Команда getent, как и полный спектр возможностей сервиса NSS, действительно используется нечасто. Сервисы часто запускаются на нестандартных портах, а нужные строки из файла /etc/passwd можно выбрать и при помощи grep. Полезной команда снова оказывается в ограниченных окружениях, где нет привычных сетевых утилит dig, host, ping, curl и любой другой, умеющей разрешать DNS-имена: в подобных случаях утилита getent может спасти ситуацию: $ sudo docker run -it --rm ubuntu [sudo] password for mint: root@0e7dbe8b1974:/# curl bash: curl: command not found root@0e7dbe8b1974:/# dig bash: dig: command not found root@0e7dbe8b1974:/# host bash: host: command not found root@0e7dbe8b1974:/# ping bash: ping: command not found root@0e7dbe8b1974:/# getent ahosts google.com 209.85.233.139 STREAM google.com 209.85.233.139 DGRAM 209.85.233.139 RAW 209.85.233.101 STREAM 209.85.233.101 DGRAM 209.85.233.101 RAW ... root@0e7dbe8b1974:/#

  • видео или голосовое, без подписи

  • Неожиданной находкой стала книга Hands-on Booting за авторством Yogesh Babar, ведущего инженера поддержки в Red Hat. Признаться, я и не думал, что в процесс загрузки ОС можно так глубоко вмешиваться и, что интереснее, необходимость что-то подкрутить действительно возникает. Приятная особенность книги, которая сразу же бросилась в глаза - автор рассказывает как о старом способе загрузки систем, использующих BIOS, так новом, для систем с поддержкой UEFI. Ранее ни в одной из книг я не встречал описания последнего способа, авторы почему-то увиливали от его описания, хотя работа с UEFI на первый взгляд стала только проще. Помимо описания нормального процесса загрузки автор не забыл привести примеры частых проблем, возникающих при запуске, объяснить причины их возникновения и дать рекомендации по их устранению. Рецепты вышли не слишком универсальными: автор фокусируется на дистрибутивах на основе Fedora, а этапы загрузки, выполняющиеся после загрузки ядра в память, уже могут отличаться в дистрибутивах. Так, например, Debian и Ubuntu 24 используют пакет initramfs-tools для управления образом загрузочного диска, а Fedora - dracut. Эти системы используют разные скрипты, имеют различную архитектуру, по-разному вовлекают systemd и используют разные команды, и всё-таки изучение любой из этих систем не будет лишним - понять вторую с имеющимся багажом знаний будет куда легче. Да и вообще, в Ubuntu 25 обещают поддержать dracut - лишний стимул начать знакомиться с ним раньше :) Не изменяя себе, издательство Apress стабильно допускает немалое количество воды в текст. Зачем-то целая глава посвящена установке сотни операционных систем на один жёсткий диск, и приведён полный многостраничный список используемых ОС. Пример вышел бы таким же наглядным, остановись автор на 10 разделах, но зачем-то главу растянули. Полезных страниц в книге всё-таки немного меньше заявленных четырёх сотен. Как обычно, оставлю несколько самых интересных заметок из книги. 1. UEFI - спецификация, разработанная для современных систем, не стеснённых в ресурсах. Наконец она позволяет не разбивать загрузчик на много частей, а использовать единый исполняемый файл. 2. Помимо загрузчика и EFI shell UEFI позволяет запускать и другие приложения. В принципе, UEFI можно расценивать как небольшую ОС. 3. Возможно, в будущем расположение файлов загрузчиков будет описываться стандартом BLS, но на данный момент стандарт распространения не получил. 4. vmlinuz - bzip-сжатый файл vmlinux c добавленным в начало архива кодом его распаковки. 5. initramfs - опционально сжатый cpio архив с файлами. 6. initramfs должен содержать все .ko -файлы, необходимые для монтирования реальной файловой системы. 7. По умолчанию в окружении initramfs монтируются только корень ФС и директория /usr. Чтобы примонтировать дополнительную директорию (например, /var для сохранения логов загрузки) добавьте её в файл fstab с опцией x-mount.target. 8. systemctl reload-daemon также запускает systemd-генераторы.

  • Система альтернатив - занятный механизм, сталкиваться с которым приходится редко. О себе он напоминает, внося элемент развлечения, когда нужно добраться до какого-нибудь стандартного исполняемого файла: # Hmm, is 'open' a binary or a script? $ command -v open /usr/bin/open $ file -b /usr/bin/open symbolic link to /etc/alternatives/open $ file -b /etc/alternatives/open symbolic link to /usr/bin/xdg-open $ file -b /usr/bin/xdg-open POSIX shell script, ASCII text executable Введена система была, чтобы иметь возможность выбирать предпочтительную утилиту среди нескольких, выполняющих схожие функции - например, текстовый редактор. Да, пользователь может выбрать его сам, используя переменные окружения EDITOR, VISUAL и иже с ними, однако в таком решении видится как минимум две проблемы: 1. Что вызывать, если пользователь не указал ничего? 2. Переменные окружения по умолчанию не сохраняются при вызове, например, sudo. Имея установленные редакторы vim и nano, нельзя позволить каждой утилите самостоятельно решать, кого из них вызвать: от беспорядочного чередования редакторов пользователь взвоет уже через пару часов. Система альтернатив решает проблему, вводя единую для всех команду editor, ссылающуюся на единый, одинаковый для всех редактор (который в конечном счёте можно переопределить переменными окружения). Аналогично появляются команды pager, view, awk, cc и другие. Как заметно в примере выше, механизм опирается на ферму ссылок в директории /etc/alternatives: универсальное имя /usr/bin/open ссылается на символическую ссылку /etc/alternatives/open, которая, в свою очередь, уже содержит путь к финальному скрипту /usr/bin/xdg-open. Первую же ссылку /usr/bin/open можно было бы направить сразу на конечный скрипт, однако тогда сменить его в некоторых конфигурациях ОС будет нельзя: нередко директория /usr/bin монтируется в режиме только на чтение, и перезаписать ссылку /usr/bin/open просто не получится. Чтобы избежать таких проблем, ферму разместили в поддиректории /etc, куда изменения обычно и вносятся, а в директории /usr/bin оставили неизменные ссылки на /etc/alternatives. При установке альтернативы ей также назначается приоритет, который позволяет выбрать утилиту в автоматическом режиме, если администратор не назначил её явно. Вся эта информация хранится в директории /var/lib/dpkg/alternatives, даже если альтернатива была установлена не из deb -пакета. Управлять ссылками вручную нет необходимости - для работы с ними используется утилита update-alternatives. Регистрация альтернатив происходит обычно при установке deb-пакета, поставщики которого сами решают, к какой группе стоит отнести утилиту. Установить свою команду можно и самостоятельно, конечно: # Run all as root # 1. Create 2 alternative scripts: $ cat /usr/local/bin/alttest.1 echo 'Hello Elliot' $ cat /usr/local/bin/alttest.2 echo 'Hello Darlene' # 2. Install alternatives: # now /usr/bin/alttest will point to scripts $ update-alternatives --install /usr/bin/alttest alttest \ /usr/local/bin/alttest.1 10 $ update-alternatives --install /usr/bin/alttest alttest \ /usr/local/bin/alttest.2 20 $ alttest Hello Darlene Теперь поменяем выбор: $ update-alternatives --list alttest /usr/local/bin/alttest.1 /usr/local/bin/alttest.2 # Set alttest.1 as default $ update-alternatives --set alttest /usr/local/bin/alttest.1 $ update-alternatives --query alttest ... Value: /usr/local/bin/alttest.1 ... $ alttest Hello Elliot # Clean up $ update-alternatives --remove-all alttest $ alttest alttest: command not found Чтобы посмотреть, какие имена установлены и альтернативы выбраны, пригодится команда update-alternatives --get-selections. К чему это всё - после установки Ubuntu не забудьте выполнить sudo update-alternatives --set editor /usr/bin/vim.basic :)

  • Продолжим обсуждение - примером "продвинутой" команды может служить обновление файла в архиве, использующее обычную операцию добавления -r/--append. Обновление файла в архиве реализовано через дописывание новой версии файла в конец архива под тем же именем: tar может содержать сразу несколько копий одного файла. Распаковка файлов происходит тоже последовательно, с начала архива, и в результате, поочерёдно извлекая файлы из архива, операция -x создаёт только последний файл, который перетрёт все извлечённые ранее: # Change contents of d.txt and update the file $ echo 321 > a/b/c/d.txt $ tar -rf archive.tar a/b/c/d.txt # See 2 versions here $ tar -vtf archive.tar drwxrwxr-x mint/mint 0 2025-04-24 00:01 a/ drwxrwxr-x mint/mint 0 2025-04-24 00:01 a/b/ drwxrwxr-x mint/mint 0 2025-04-24 00:01 a/b/c/ -rw-rw-r-- mint/mint 4 2025-04-24 00:01 a/b/c/d.txt -rw-rw-r-- mint/mint 4 2025-04-24 00:21 a/b/c/d.txt # `tar -x` would create the last version of d.txt $ rm -rf a/ $ tar -xf archive.tar $ cat a/b/c/d.txt 321 Что интересно, операция извлечения файла действительно сначала создаст старую версию файла, и затем заменит её новой. Это видно, если использовать ключик -O/--to-stdout, печатающий содержимое файлов вместо их создания: $ tar -xf archive.tar a/b/c/d.txt -O 123 321 Извлечь конкретную версию можно, указав её номер при помощи ключа --occurence: # Note creation time $ tar -vtf archive.tar --occurrence=1 a/b/c/d.txt -rw-rw-r-- mint/mint 4 2025-04-24 00:01 a/b/c/d.txt # Extract file to stdout: old contents! $ tar -xf archive.tar --occurrence=1 a/b/c/d.txt -O 123 # New contents are here as well $ tar -xf archive.tar --occurrence=2 a/b/c/d.txt -O 321 Этакий бекап для бедных :) Кроме того, самому архиву можно назначать метку. Это может пригодиться, чтобы, например, наверняка сохранить время создания бекапа: $ tar -cf backup.tar a/ -V "Created at $(date)" # --list shows the label! $ tar -tf backup.tar created at Thu Apr 24 23:23:04 MSK 2025 a/ a/b/ a/b/c/ a/b/c/d.txt Утилита GNU tar предлагает огромное количество прочих полезных действий, применение которым найти уже сложнее, и не умещаются в заметку. Скажем, tar можно заставить вызвать интерактивную сессию Bash # Press `!` after prompt $ tar -cf tmp.tar a/ -L 2 Prepare volume #2 for ‘tmp.tar’ and hit return: ~ $ echo SHLVL 2 или разбить архив на несколько файлов, каждый из которых поместить на отдельный диск - на случай, если архив слишком большой. Документация GNU tar однозначно заслуживает изучения. Если вам приходилось использовать непопулярные возможности tar, буду рад изучить их в комментариях к заметке. Уверен, найдутся действительно стоящие идеи!

  • Предыдущая заметка побудила к изучению формата файлов tar, и, как оказалось, переписывание путей извлекаемых файлов - не единственная неожиданная операция над tar-архивами. Изначально архив проектировался для хранения файлов на магнитной ленте (отсюда и имя - Tape ARchive), которая поддерживает только последовательную запись - это наложило свой отпечаток на структуру архива. За время его существования родилось несколько схожих форматов файла архива, отличающихся деталями, такими как поддержка длинных имён, хранение разреженных и специальных файлов и т.д. Наиболее популярным форматом будет, наверное, ustar, хотя и свежий pax тоже набирает обороты. Вне зависимости от используемого формата архив будет представлять собой последовательность записей, завершающейся маркером конца архива: двумя блоками нулей по 512 байтов. Каждая запись, в свою очередь, представляет из себя заголовок сохранённого файла, за которым следует его содержимое. Описание полей заголовка можно найти в документации, с которой в редких случаях полезно проконсультироваться: файлы могут обладать множеством атрибутов, и не для всех найдётся место в заголовке. В простейшем случае архив будет содержать несколько записей, каждая из которых описывает свой файл. Имя файла "отсчитывается" от текущей директории, и, если путь до файла включает в себя несколько вложенных директорий, их имена просто добавляются в сохранённое имя. Ключ -t/--list как раз выводит список имён всех сохранённых в архиве файлов: # Create test file hier $ mkdir -p a/b/c $ echo 123 > a/b/c/d.txt $ tar -cf archive.tar a/ # Show each stored file name $ tar -tf archive.tar a/ a/b/ a/b/c/ a/b/c/d.txt Такая простая схема записи имени файла и позволяет выполнить трюк из предыдущей заметки. А ещё к архиву можно прикрепить данные, не предназначенные для посторонних глаз: tar игнорирует всё после маркера конца файла: # Add a secret string after 2 terminating zero blocks $ echo 'Hello Elliot' >> archive.tar # Nothing suspicious... $ tar -tf archive.tar a/ a/b/ a/b/c/ a/b/c/d.txt # ...but secret string is still here! $ tail -n1 archive.tar Hello Elliot Конечно, особо внимательный исследователь найдёт расхождение в размере файла, однако когда-нибудь эта лазейка может и пригодиться. Над другими базовыми операциями над архивом задерживаться не интересно, а вот "продвинутые" операции (create/update/delete) уже заслуживают внимания, попутно демонстрируя наследие магнитных лент: все изменения вносятся в конец архива, команды практически никогда не переписывают уже имеющееся содержимое - о них в следующей части заметки.

  • Заметка дня #10 Получая архив откуда-либо, его содержимое всегда полезно проверить перед распаковкой - в противном случае следующие за ней 15 минут можно потратить, собирая рассыпавшиеся по всей директории файлы. Иногда поместить файлы в общую директорию в архиве просто нельзя (например, формат APK накладывает свои ограничения), а иногда создатель архива решает не заморачиваться. Если нам не повезло и файлы не собраны в общей директории, то самый простой и надёжный способ обезопаситься от "сбежавших" файлов - создать временную пустую директорию, переместить в неё архив и распаковать его там. В случае с GNU tar ключ -C позволяет указать имя директории, в которую надо извлечь файлы, но первый шаг всё равно придётся выполнить: tar не создаст эту директорию, если её не существует: # Create some archive $ mkdir -p a/b/c $ echo 123 > a/b/c/d $ tar -cf some_archive.tar a # Look at files inside $ tar -tf some_archive.tar a/ a/b/ a/b/c/ a/b/c/d # Try to unpack to a non-existent directory $ tar -xf some_archive.tar -C tmp_dir tar: tmp_dir: Cannot open: No such file or directory tar: Error is not recoverable: exiting now Фокус, однако, можно провернуть, воспользовавшись менее популярным ключом --transform, принимающим выражение sed для преобразования имён файлов: $ tar -tf some_archive.tar \ --transform 's,^,tmp_dir/,' --show-transformed-names tmp_dir/a/ tmp_dir/a/b/ tmp_dir/a/b/c/ tmp_dir/a/b/c/d $ tar -xf some_archive.tar --transform 's,^,tmp_dir/,' $ cat tmp_dir/a/b/c/d 123 В примере выше мы заменили в названиях файлов все начала строк (`^`) на имя директории tmp_dir/, и tar при распаковке архива создал её, принимая за часть оригинального названия файла. Действие ключа --transform не ограничено добавлением префиксов - имена файлов можно крутить, как только позволит фантазия: # Substitute b for tmp_dir $ tar -tf some_archive.tar \ --show-transformed-names --transform 's,b,tmp_dir,' a/ a/tmp_dir/ a/tmp_dir/c/ a/tmp_dir/c/d # Remove b subdirectory (// is the same as /) $ tar -tf some_archive.tar \ --show-transformed-names --transform 's,b,,' a/ a// a//c/ a//c/d Здесь важно заметить, что tar по умолчанию не создаёт файлы по абсолютным путям и всегда отбрасывает ведущий первый символ / из их имён. Замена s,^,/tmp/, распакует архив не в директорию /tmp/ в корне ФС, а в tmp/, созданную в текущей директории, и выведет предупреждение: tar -tf some_archive.tar \ --transform 's,^,/tmp/,' --show-transformed-names tar: Removing leading `/' from member names tmp/a/ tmp/a/b/ tmp/a/b/c/ tmp/a/b/c/d Если мы уверены, что всё делаем верно и нам действительно нужны абсолютные пути, то ключом -P можно разрешить создавать такие файлы: $ tar -tf some_archive.tar \ --transform 's,^,/tmp/,' --show-transformed-names -P /tmp/a/ /tmp/a/b/ /tmp/a/b/c/ /tmp/a/b/c/d #tip

  • видео или голосовое, без подписи

  • Не всем моим отзывам о литературе суждено быть положительными - всё-таки и в проверенные издательства попадают книги неоднозначного содержания. Practical Vulnerability Management стала одной из них. Судя по высокому среднему баллу издания на Amazon, я просто мог не попасть в число тех, для кого эта книга была написана, и даже с учётом этого остаются вопросы к содержимому книги. Издание поделено на две части, теоретическую и практическую. Первая часть посвящена обсуждению общих вопросов безопасности, и в этот момент также становится ясно, кто должен извлечь наибольшую пользу из текста: автор планирует строить ИБ с нуля. Начинается книга с введения читателя в курс дела, рассказа о номенклатуре, инструментах, подходах, корпоративных политиках безопасности, важности коммуникации с соседними командами и прочих общих вещах. Для человека, совершенно не знакомого с темой, подобное вступление может показаться интересным даже несмотря на то, что некоторые страницы очень хочется сократить до "делайте хорошо, а плохо не надо" - уж больно очевидные и расплывчатые бывают советы. Много рассуждений посвящено расположению сканеров, их настройке, сетевым особенностям, ограничениям сетевых сканеров и прочим сложностям - общими словами автор ограничился и тут. Следом за введением идёт длинная практическая часть, которая безостановочно вызывала удивление при прочтении. На протяжении большей части книги автор развивал своё решение для анализа безопасности инфраструктуры компании, пользуясь только доступными инструментами с исходным открытым кодом: задумка вызывает интерес, реализация - только недоумение. Система строится из пачки скриптов на Python и Bash, которые обрабатывают XML-вывод nmap и OpenVAS и сохраняют результат в запущенную под ногами MongoDB. Зачем-то к этому прикручивается реализованное наполовину REST API, а сами скрипты запускаются при помощи cron. Итоговая система, похоже, отвечает всем требованиям - позволяет регулярно оценивать защищённость инфраструктуры и получать отчёты в различных форматах для выработки дальнейших действий. Странно здесь то, что система описывается как важный компонент безопасности компании, однако её собственная надёжность в требования не вошла. Жизненный цикл практически не обсуждается, обслуживание сводится к надежде на работу cron, о планах экстренного восстановления не говорится ни слова. Человеку без опыта администрирования будет крайне сложно найти ошибку в такой написанной на коленке многокомпонентной системе, опытному же инженеру достаточно будет дать задание вида "сохрани вывод сканеров, обрезав маловажное" - мне действительно не ясно, какой уровень подготовки ожидался от читателя. Пару слов можно сказать и о самих скриптах. Автор аскетичен и избегает сторонних Python-библиотек, аргументируя это лишними зависимостями, в чём его хочется только поддержать, однако выражается это в виде велосипедов и дополнительного слоя обёрток на Bash. Кажется, что использование, скажем, python3-nmap и gvm-tools значительно упростило бы код и не столь уж запутало бы зависимости. И всё-таки завершить описание хочется на ноте позитивной. С огромным интересом я отметил, что эксплуатацию уязвимостей также можно пробовать автоматизировать. Автору предельно хорошо удалось развернуть мысль, что безопасность - это процесс, а не конечный результат. Наконец, откровенно вредных советов я не увидел, что немаловажно. Не думаю, что я стану советовать кому-то прочитать эту книгу, однако и существенных поводов отговаривать от знакомства с ней тоже назвать не могу.

  • Заметка дня #9 Управление трафиком - занятие, требующее особого внимания. Настраивая VPN или просто играясь с таблицами маршрутизации, обычно хочется понять, применились ли настройки ожидаемым образом. В общем случае решение задачи заключается в чтении таблицы маршрутизации, однако распутывание клубка может затянуться. Мало того, что жонглирование масками и адресами требует сноровки, так ещё и Linux использует Policy Based Routing: ядро поддерживает несколько таблиц маршрутизации и для каждого пакета выбирает нужную на основе правил, которые, в свою очередь, могут использовать метки, навешиваемые на пакет фаерволом. Так, например, поднятый при помощи wg-quick VPN в Нидерланды может корректно работать, но совершенно не будет заметен в выводе команды ip route show: $ ip route show default via 192.168.1.1 dev wlp0s20f3 proto dhcp metric 600 169.254.0.0/16 dev wlp0s20f3 scope link metric 1000 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 192.168.1.0/24 dev wlp0s20f3 proto kernel scope link src 192.168.1.13 metric 600 # Note: interface 'netherlands' is up! $ ip -br link lo UNKNOWN 00:00:00:00:00:00 enp58s0 DOWN 95:85:36:4a:ee:7f wlp0s20f3 UP 2f:61:0a:d7:6f:59 docker0 DOWN 05:b2:b8:f7:70:91 netherlands UNKNOWN <POINTOPOINT,NOARP,UP,LOWER_UP> Обнаружить искомую таблицу маршрутизации всё-таки можно, подглядев её номер в выводе команды ip rule: $ ip rule 0: from all lookup local 32764: from all lookup main suppress_prefixlength 0 32765: not from all fwmark 0xca6c lookup 51820 32766: from all lookup main 32767: from all lookup default # Use table number 51820 to see routes: here it is! $ ip route show table 51820 default dev netherlands scope link Часть с fwmark 0xca6c навешивается через nftables и её тоже можно найти, однако уже сейчас видно, какой глубины расследование приходится проводить - и даже после этого не вполне очевидно, как убедиться, что трафик на определённые адреса пойдёт через VPN, а не другие интерфейсы. Такое длительное вступление позволит лучше оценить команду ip route get, ради которой и затевалась заметка. Команда позволяет узнать, как в итоге ядро будет маршрутизировать пакет с заданным адресом назначения: $ ip route get 1.1.1.1 1.1.1.1 dev netherlands table 51820 src 10.20.30.40 uid 1000 cache $ ip route get 192.168.1.12 192.168.1.12 dev wlp0s20f3 src 192.168.1.13 uid 1000 cache man ip-route подробно рассказывает, какие ещё параметры пакета помимо адреса назначения можно указать: интерфейс прибытия пакета, адрес отправителя, порт, метка фаервола и прочее - команда может значительно облегчить жизнь при настройке сети. #tip

  • Самое, наверное, заметное отличие компилируемых языков от интерпретируемых, заключается в возможности прочитать код программы. Чтобы понять по исполняемому файлу, что он делает, нужно применять специальные инструменты - отладчики, шестнадцатеричные редакторы, дизассемблеры - и долго и упорно изучать вывод этих команд. В то же время скрипты и библиотеки Python можно спокойно прочитать и даже поправить без вреда для программы. Это очень удобно для отладки, однако иногда всё-таки хочется сохранить код в секрете от пользователя. Полноценная защита от вскрытия - отдельная и большая задача. В большинстве случаев, к счастью, достаточно просто спрятать исходный код в надежде, что разбираться с ним будет просто лень. Из бинарных файлов для этого достаточно вырезать таблицу символов, а скрипты - обфусцировать, бессмысленно изменив имена переменных и внеся избыточную логику, или превратить их в бинарные файлы, следуя методу чайника. Для Python существуют даже онлайн-сервисы, выполняющие подобную работу. И всё-таки в случае с Python можно обойтись без таких ухищрений, тем более, что восстановить исходные коды после подобных преобразований не столь сложно. При ближайшем рассмотрении Python, как и многие другие интерпретируемые языки, перед исполнением скрипта компилирует его в байт-код, который уже исполняется виртуальной машиной интерпретатора. После запуска любого проекта, написанного на Python, в рабочей директории создаётся директория __pycache__/, содержащая .pyc -файлы - скомпилированные представления каждого из модулей: $ ls -1 main.py utils.py $ cat * # main.py import utils print(utils.hi) # utils.py hi = 'hi' $ python3 main.py hi $ ls -R1 .: __pycache__ main.py utils.py ./__pycache__: utils.cpython-312.pyc За компиляцию отвечают встроенные модули Python py_compile и compileall, которые можно вызвать явно, что мы и сделаем, чтобы скомпилировать файл main.py: $ python3 -m py_compile main.py $ ls -1 __pycache__ main.cpython-312.pyc utils.cpython-312.pyc Это представление уже достаточно надёжно "прячет" код от пользователя, а главное, его можно использовать и в отсутствие файлов с исходным кодом, что в значительной мере решает поставленную задачу: # Remove source code $ rm *.py # Place .pyc-files in place of .py $ mv __pycache__/* . $ rm -rf __pycache__; ls -1 main.cpython-312.pyc utils.cpython-312.pyc # Run! $ python3 main.cpython-312.pyc hi Решение, прибегающее к "sourceless Python", всегда будет требовать той же версии Python, для которой был сгенерирован байт-код: промежуточное представление кода часто меняется, и Python3.12 почти наверное не сможет прочитать .pyc -файл версии 3.11. В прочем, если код поставляется в виде docker-образа, то согласовать версию интерпретатора с байт-кодом будет не так сложно. Пользователь также может попытаться восстановить исходный код с помощью декомпилятора, например, decompyle, но угнаться за стандартом не так-то просто - байт-код для версий свежее, чем 3.8, восстановить на момент написания заметки скорее всего pycache$ decompyle3 __pycache__/utils.cpython-312.pyc ... Unsupported Python version, 3.12.0, for decompilation ...

  • видео или голосовое, без подписи

  • Найти более-менее полноценное описание возможностей SystemD, оказывается, не так-то просто. С той же проблемой столкнулся и Donald A. Tevault, автор книги с длинным, но говорящим названием Linux Service Management Made Easy with systemd, где этот недостаток наконец устраняется. К издательству Packt я отношусь с настороженностью, однако эта книга с лихвой оправдала возложенные на неё ожидания. SystemD - сложная многокомпонентная система, включающая несколько консольных утилит и разных типов файлов для управления конфигурацией, она тесно интегрирована в современные дистрибутивы. SystemD обладает неплохой документацией, по которой, однако, не так-то просто ориентироваться, а самостоятельно вычленить из неё все составные части и общую архитектуру - и вовсе непосильная задача. Книга закрывает все эти пробелы. Объём заметок на этот раз зашкаливает, поэтому для сохранения здесь пришлось выбрать лишь самые полезные и прикладные - если вам приходится работать с SystemD, настоятельно советую познакомиться с книгой целиком и самостоятельно. Уверен, потраченное на её изучение время окупится сполна. 1. Найти описание конкретных директив unit-файлов бывает сложно. Для этого проект поддерживает единый индекс директив со ссылками на страницы man, где можно найти их детальное описание: man systemd.directives. 2. Секция [Install] unit-файлов влияет только на поведение сервиса при вызове команды systemctl enable: она получила своё название в честь того, что описывает, как создавать символические ссылки на этот файл. Отсутствие этой секции делаем сервис статическим - он может быть активирован лишь другими unit-файлами. 3. systemctl kill отправляет сигнал всем процессам, порождённым сервисом - это значительно удобнее и правильнее, чем находить все связанные процессы и отправлять сигнал каждому из них самостоятельно. 4. Новые и модифицированные unit-файлы стоит класть в директорию /etc/systemd/system/, а не /lib/systemd/system: первые имеют приоритет. Этим же пользуется команда systemctl mask, создающая в /etc/systemd/system ссылку на /dev/null с именем сервиса. 5. Команда systemd-analyze позволяет отлаживать многие вещи. Среди прочего, команда systemd-analyze security покажет, какие именно настройки безопасности стоит поправить для выбранного сервиса. 6. Страница man systemd.special содержит описание многих "системных" unit-файлов. 7. Команда systemctl isolate позволяет загрузить выбранный target - своего рода гибкий аналог SysV runlevel. 8. SystemD генерирует unit-файлы для старта стандартных init-скриптов, обеспечивая обратную совместимость. 9. Нажатие ctrl+alt+del 7 раз в течение 2 секунд перезагружает компьютер. Это работает только в текстовом режиме (графическая оболочка перехватывает эту комбинацию клавиш) и может быть отключено маскированием ctrl-alt-del.target. 10. Команда systemctl poweroff --force --force (ключ --force намеренно указан дважды) позволяет выключить компьютер без обращения к SystemD. Это может быть полезно, если демон по каким-то причинам дал сбой. 11. Для просмотра конфигурации NUMA есть команда numactl. 12. journald сохраняет данные перманентно только при существовании директории /var/log/journal. В противном случае логи сохраняются в директорию /run/log/journal и удаляются при перезагрузке.

  • Задача атакующего всегда будет проще задачи обороняющегося - для победы первым достаточно найти лишь один недочёт в работе вторых. Руководствуясь этой логикой, опытные администраторы советуют не трогать лишний раз стандартные, выверенные настройки безопасности без веских причин и чёткого понимания своих действий: современные системы сложные, многокомпонентные, и упустить какую-то деталь может быть слишком просто. Внимательно рассматривая предыдущую заметку, можно задуматься, как именно polkit определил, что пользователю можно доверять. Для Ubuntu ответ находится в man pklocalauthority: polkit читает файлы конфигурации из директории /etc/polkit-1/, где описано, какие пользователи считаются администраторами: $ cat /etc/polkit-1/localauthority.conf.d/* [Configuration] AdminIdentities=unix-user:0 [Configuration] AdminIdentities=unix-group:sudo;unix-group:admin Авторизовать как администраторов polkit будет пользователя с id 0, то есть root, и всех пользователей, помещённых в группы sudo или admin. Для других дистрибутивов настройки могут немного отличаться, однако принцип сохраняется. Например, в RedOS 8 этих файлов не будет, зато одно из правил в директории /usr/share/polkit-1/rules.d/ будет гласить: # cat 50-default.rules polkit.addAdminRule(function(action, subject) { return ["unix-group:wheel"]; }); Получается, любой пользователь из группы wheel будет считаться администратором. Это наблюдение интересно тем, что sudo позволяет точечно выдавать пользователям права на исполнение определённых файлов. Например, строки mint ALL =(ALL) !ALL mint ALL = NOPASSWD: /usr/bin/ls в конфиге sudo позволяет пользователю mint выполнять только команду ls от имени суперпользователя, даже если он находится в группе sudo: $ id -nG mint adm sudo docker $ sudo touch file.txt Sorry, user mint is not allowed to execute '/usr/bin/touch' as root on this-host. $ sudo ls Audio Desktop ... Однако pkexec об этих настройках не знает, поэтому трюк из предыдущей заметки работает. И это же заставляет задуматься о том, что одного членства в группе администратора достаточно для получения полных прав вне зависимости от настроек в файле /etc/sudoers. Нельзя добавлять пользователей в админские группы, желая выдать им лишь частичные права на исполнение файлов. Дистрибутивы определяют самостоятельно, какие группы будут давать доступ к командам sudo и pkexec, и согласованно составляют файлы конфигурации для обеих систем. Менять их тоже нужно совместно и осторожно. Интересно, что даже CIS Bencmarks практически не касаются этой темы. Можно заметить, кстати, что такие "топорные" политики polkit объясняют, почему документация и установщик говорят об "администраторе", а не просто о "доступе к sudo ". Разница всё же есть. P.S. Вызывая pkexec в графическом интерфейсе Gnome, polkit не позволит выбрать пользователя, от чьего имени авторизоваться, доступен будет только первый созданный в системе пользователь. Этому багу уже больше 4 лет, и до сих пор его не починили. Да и ладно, зато pkttyagent этим не болеет.

Остальные 90% — tgindex