惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
V
V2EX
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Y
Y Combinator Blog
小众软件
小众软件
Jina AI
Jina AI
V2EX - 技术
V2EX - 技术
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
The Hacker News
The Hacker News
C
Cybersecurity and Infrastructure Security Agency CISA
K
Kaspersky official blog
博客园 - 三生石上(FineUI控件)
T
Threatpost
博客园 - 聂微东
Scott Helme
Scott Helme
IT之家
IT之家
N
Netflix TechBlog - Medium
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
MongoDB | Blog
MongoDB | Blog
T
Tor Project blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
About on SuperTechFans
酷 壳 – CoolShell
酷 壳 – CoolShell
C
CERT Recently Published Vulnerability Notes
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
C
Check Point Blog
L
LINUX DO - 最新话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
Attack and Defense Labs
Attack and Defense Labs
T
Tailwind CSS Blog
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Webroot Blog
Webroot Blog
Help Net Security
Help Net Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
«У меня работает»: десять способов узнать, что нет
trgv · 2026-06-06 · via Все публикации подряд на Хабре

У меня был «полностью готовый» проект. Демон виртуального последовательного порта vseriald собирался у меня на машине четырьмя пресетами, проходил все тесты, был чист под valgrind, генерировал документацию без единого предупреждения. Я решил, что пора выпускать версию 1.0, поставил тег — и впервые по-настоящему запустил CI целиком.

Дальше был парад граблей. Не один-два «упс», а десяток подряд: пайплайн то не компилировался вовсе, то падал на сборке, то на скачивании артефактов, то на упаковке. И почти каждая проблема была невидима на машине разработчика по совершенно банальной причине: у меня другая версия компилятора, другая версия CMake, другой Doxygen и другая сеть, чем на чистом раннере.

Это и есть мораль, которую вынесу вперед: CI ловит ровно то, что не видно локально — версии инструментов, окружение и сеть. В этой статье — конкретные грабли по порядку, с настоящими сообщениями об ошибках, причинами и тем, как я их чинил. А в конце — как из всего этого выросли два полезных приема: воспроизведение окружения у себя и ускорение пайплайна в три с лишним раза.

Проект, о котором речь, открыт (читать, форкать, заводить задачи): https://gitlab.com/trgv/vspd. Это вторая из двух статей. О внутренностях самого демона (псевдотерминал, цикл событий, эмуляция RS485) — в первой, «COM-порт из ничего: PTY, epoll и немного RS485-боли»; здесь только про дорогу к релизу.

Вводные данные

CI у меня на GitLab. Пайплайн по стадиям: проверка стиля → сборка (матрица: Ubuntu 22.04 и 24.04 × GCC и Clang, плюс параллельная сборка Bazel) → тесты → анализ безопасности → санитайзеры (ASan + UBSan) → valgrind → документация (Doxygen) → упаковка (.deb и архив исходников) → публикация релиза.

Важная деталь: матрица намеренно гоняет сборку и на 22.04, и на 24.04. Это разные компиляторы (GCC 11 против GCC 13), разные версии CMake (3.22 против 3.28) и разные версии вспомогательных утилит. Как выяснилось — именно эти различия и ловили почти все.

И еще одна вводная: бесплатные общие раннеры GitLab у меня к тому моменту исчерпали лимит минут, поэтому пайплайн поехал на собственном раннере (Docker executor), поднятом на отдельном сервере. Часть «сетевых» грабель вылезла именно из-за этого — раннер живет в другой сети, чем инфраструктура GitLab.

Грабли по порядку

1. Пайплайн не компилируется вообще

Самый первый запуск не дошел даже до сборки: GitLab отказался разбирать конфигурацию.

jobs:...:cache:key:files config has too many items (maximum is 2)

Оказывается, в cache:key:files можно указать максимум два файла. У меня их было три. Чинится в одну строку — оставить два самых важных. Урок скучный, но честный: серверный линтер конфигурации (glab ci config compile или вкладка CI Lint) надо прогонять до пуша, а не после.

2. Версия пресетов против CMake на Ubuntu 22.04

Дальше посыпались задачи сборки на 22.04:

CMake Error: Could not read presets ... Unrecognized "version" field

В CMakePresets.json стояла "version": 6. А шестая версия схемы пресетов требует CMake 3.25+. На Ubuntu 22.04 системный CMake — 3.22, и он эту версию не понимает. При этом я в требованиях сам же написал «CMake >= 3.22» — то есть нарушил собственный минимум.

Фикс: понизил версию схемы до 3 (я реально использовал только возможности, доступные с 3.21 — пресеты сборки, тестов, наследование). После этого пресеты читаются и на 3.22, и на новых. Урок: заявленный минимальный CMake надо проверять на этом самом минимальном, а не на том, что стоит у тебя.

3. FetchContent и ключи, которых нет в старом CMake

Пресеты читаются, но сборка на 22.04 падает на этапе генерации: не найдены цели зависимостей.

Target "vseriald_unit_tests" links to target "spdlog::spdlog" but the
target was not found.

Я подтягиваю зависимости (spdlog, yaml-cpp, googletest) через FetchContent, и в объявлении писал так:

FetchContent_Declare(spdlog ... EXCLUDE_FROM_ALL SYSTEM)

Ключевые слова SYSTEM (пометить заголовки как системные) и EXCLUDE_FROM_ALL появились у FetchContent_Declare только в CMake 3.25 и 3.28 соответственно. На 3.22 они ломают подтягивание — и цели зависимостей просто не создаются.

Убрать их нельзя бездумно: пометка SYSTEM мне нужна, чтобы строгий -Werror не валился на предупреждениях из чужих заголовков (того же spdlog). Поэтому я убрал несовместимые ключи, а «системность» включаемых каталогов вернул через свойство цели, которое работает и на старом CMake:

# Заголовки зависимостей — как системные, чтобы -Werror не падал на чужом
# коде. Ключ SYSTEM у FetchContent_Declare требует CMake 3.25; это свойство
# делает то же самое на базовой версии 3.22.
function(_vseriald_mark_includes_system tgt)
  if(TARGET ${tgt})
    get_target_property(_inc ${tgt} INTERFACE_INCLUDE_DIRECTORIES)
    if(_inc)
      set_target_properties(${tgt} PROPERTIES
                            INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${_inc}")
    endif()
  endif()
endfunction()

4. GCC 11 строже нового GCC

Сборка 22.04 под GCC доехала до компиляции и упала:

error: useless cast to type 'mode_t' {aka 'unsigned int'} [-Werror=useless-cast]

В коде был static_cast<mode_t>(mode), где mode уже имеет тип mode_t. Новый GCC у меня на машине такой каст лишним не считает, а GCC 11 на 22.04 — считает, и с -Werror это ошибка. Фикс тривиальный — убрать лишний каст:

// было:  ::chmod(path.c_str(), static_cast<mode_t>(mode));
::chmod(path.c_str(), mode);

Урок прямой: матрица компиляторов нужна не для галочки; разные версии — разный набор предупреждений.

5. Артефакты между задачами и таинственный 403

Сборка прошла, но тестовые задачи, которым нужен результат сборки, не смогли его скачать:

Downloading artifacts for build:cmake:gcc:debug:24.04 ...
ERROR: Downloading artifacts from coordinator... 403 Forbidden

Раннер пытался забрать артефакт напрямую с CDN GitLab (cdn.artifacts.gitlab-static.net), получал редирект и упирался в 403. На общих раннерах GitLab это работает, на моем — нет. Лечится флагом раннера FF_USE_DIRECT_DOWNLOAD=false: тогда артефакты качаются через координатор, а не напрямую с CDN. После перезапуска раннера тестовые задачи позеленели. Урок: на собственном раннере «прямое скачивание» артефактов может не пускать — это известная особенность.

6. ASan-бинарь под valgrind

Стадия санитайзеров упала странно:

==NNNN== Memcheck, a memory error detector
ASan runtime does not come first in initial library list ...

Видно, что ASan-собранный бинарь зачем-то запускается под valgrind. А ASan и valgrind несовместимы в принципе. Причина: вспомогательная функция в CMake регистрировала «valgrind-обертки» для тестов всегда, когда в системе есть valgrind — не глядя на то, что текущая сборка уже инструментирована санитайзером. В санитайзер-окружении valgrind тоже стоял, и обертки регистрировались поверх ASan-бинарей.

Фикс: в санитайзер-сборке не регистрировать ни valgrind-, ни callgrind-тесты вообще. Я это и в документации писал — «санитайзеры несовместимы с valgrind», — но в самой конфигурации сборки это правило раньше нигде не было прописано. Урок: правило, записанное только в документации, рано или поздно нарушится в коде; его надо выражать в самой системе сборки.

7. Тайминговые тесты не дружат с инструментацией

Следом в той же стадии иногда падал тест «медленного потребителя» — тот, что проверяет: если получатель не успевает, демон притормаживает источник и ничего не теряет. Под замедлением ASan тайминги «плыли», и тест становился нестабильным (в одном прогоне зеленый, в другом красный).

Эти тесты завязаны на заполнение буферов ядра и EAGAIN, и под санитайзером или valgrind, которые все заметно замедляют, это ненадежно. Я исключил их из санитайзер-прогона — ровно так же, как они уже были исключены из прогона под valgrind. Их память и так покрыта модульными тестами, а полную «тяжелую» проверку делают обычные, неинструментированные тесты. Урок: тайминг-зависимые тесты и санитайзеры/valgrind — плохие соседи; разделяйте.

8. Doxygen на раннере не такой, как у вас

Стадия документации с WARN_AS_ERROR упала на десятке предупреждений вида:

warning: unable to resolve reference to 'kBlock' for \ref command

На 22.04 Doxygen — версии 1.9.1, и он не умеет разрешать ссылку на элемент перечисления (@ref kBlock, где kBlock — значение enum). Мой локальный Doxygen новее и такие ссылки разрешает, поэтому локально все было чисто. Фикс: для ссылок, которые старый Doxygen не тянет, использовать обычный код в кавычках вместо @ref. Полный список предупреждений Doxygen выдает за один прогон, так что чинится разом. Урок: версия генератора документации на раннере — тоже часть окружения.

9. Упаковка тянет неочевидное

Дошли до сборки .deb:

CPackDeb: file utility is not available ...
CPack Error: Error when generating package

Оказалось, cpack -G DEB для вычисления зависимостей пакета зовет утилиту file, а ее в минимальном образе не было. Одна строка в установку зависимостей — и пакет собирается. Урок: упаковка тянет вспомогательные утилиты, которых нет в чистом образе; их легко забыть, потому что локально они обычно уже установлены по умолчанию.

10. Реестр контейнеров недоступен из сети раннера

Самая поучительная сетевая “заковыка”. Задачи анализа безопасности (и позже — публикация релиза через служебный образ) не смогли скачать свои образы:

dial tcp 35.227.35.254:443: i/o timeout

При этом gitlab.com (git), Docker Hub и другие адреса с того же сервера были доступны — а вот registry.gitlab.com упорно отваливался по таймауту. Локальный фаервол открыт, трассировка маршрута доходит до сети Google — значит, блокировка где-то выше, у провайдера, и именно до конкретного адреса реестра. Это оказалось проблемой сети сервера, а не конфигурации. Об этой “заковыке” — отдельный разговор ниже, потому что у нее обнаружилась вторая голова.

Метод: воспроизводим окружение локально

После третьей-четвертой «версионной» грабли я перестал чинить их по одной через очередь CI (каждый прогон — это минуты ожидания) и сделал очевидное: воспроизвел проблемное окружение у себя.

  • Поставил себе ровно CMake 3.22.6 в отдельное изолированное окружение и пересобрал проект под ним с -Werror. Это сразу подтвердило, что фикс с пресетами и INTERFACE_SYSTEM_INCLUDE_DIRECTORIES действительно совместим с 3.22 — без гадания.

  • Поставил g++-11 и собрал весь проект им. Один прогон показал все места, которые не нравятся именно одиннадцатому GCC, а не по одному через CI.

Это банально, но это и есть главный вывод про «версионные» грабли: не угадывай, что не так на старом тулчейне, — поставь этот старый тулчейн рядом и собери. Полчаса на установку экономят часы хождения по кругу с пайплайном.

Свой раннер: когда кончилась квота

Параллельно со всем этим закончился лимит минут общих раннеров, и пайплайн встал в очередь «ожидания». Поэтому я поднял собственный раннер на отдельном сервере (Docker executor) и подключил его к проекту как проектный раннер, который берет задачи без тегов.

Сам по себе раннер ставится за пять минут, но именно он обнажил сетевые грабли из пунктов 5 и 10 (другая сеть) и потребовал двух настроек, которые на общих раннерах сделаны за тебя:

  • FF_USE_DIRECT_DOWNLOAD=false — чтобы артефакты качались через координатор (см. грабли 5).

  • Монтирование docker-сокета в задачи — это понадобилось чуть позже, чтобы собирать собственный образ прямо на хосте раннера (об этом — следующий раздел).

Вывод: «поднять раннер» — это не только gitlab-runner register. Это еще и понять, что сеть и окружение раннера теперь твои, а не GitLab.

Релиз руками, когда раннеров нет

Пока квота общих раннеров была исчерпана, а свой еще не стоял, релиз 1.0 надо было как-то выпустить. Оказалось, для этого CI не обязателен: все, что делает стадия публикации, несложно повторить вручную.

  • Архив исходников — это git archive из тега: git archive --format=tar.gz --prefix=vseriald-1.0.0/ v1.0.0 -o vseriald-1.0.0.tar.gz.

  • Пакет .deb — это локальная сборка release-пресета и cpack -G DEB.

  • Сам релиз с прикрепленными файлами — одна команда glab release create с этими артефактами.

Так я выпустил 1.0.0 руками. А когда поднял свой раннер и наладил доступ к реестру, следующий релиз (1.0.1) проехал по полному пайплайну сам: собрал .deb для обеих Ubuntu, архив исходников и создал релиз без единого ручного шага. Полезно знать оба пути: автоматический — норма, ручной — страховка на случай, когда инфраструктура недоступна.

Ускорение: предсобранный образ

Когда пайплайн наконец поехал целиком, всплыла новая боль: каждая задача начинала с установки всего тулчейна — компиляторов, CMake, valgrind, Doxygen, утилит. На матрице из десятка задач это съедало кучу времени: полный прогон занимал около 53 минут.

Решение классическое — предсобранный образ. Я вынес весь тулчейн в собственный образ (по одному на каждую базовую Ubuntu):

ARG BASE=ubuntu:24.04
FROM ${BASE}
RUN apt-get update -qq && apt-get install -y --no-install-recommends \
      build-essential cmake ninja-build git clang clang-format clang-tidy \
      valgrind pkg-config file libsystemd-dev doxygen graphviz shellcheck ... \
 && rm -rf /var/lib/apt/lists/*
# + python-инструменты и bazelisk

Задачи стали стартовать уже готовыми, без установки пакетов на каждый запуск. Полный прогон ужался примерно до 15 минут — в три с лишним раза быстрее.

До и после

До и после

Дальше нужно было решить, кто и когда пересобирает этот образ. Я сделал отдельную задачу build:image, которая пересобирает и публикует образ автоматически, когда меняется его описание (ci/Dockerfile), и доступна вручную в остальных случаях. Тонкость, из-за которой я выбрал сборку на хостовом docker-демоне раннера (а не отдельным инструментом): раннер использует образ по политике «нет локально — скачай». Если собрать образ другим инструментом и просто запушить в реестр, локальная копия на раннере с тем же тегом останется старой, и задачи будут брать ее, а не свежую. Сборка через хостовый docker одновременно обновляет и реестр, и локальный кэш раннера — устаревания не возникает.

Вывод: установка тулчейна в каждой задаче — это самый дешевый способ потерять время; предсобранный образ окупается с первого же полного прогона.

Реестр: две головы одного дракона

Вернусь к граблям 10. Доступ к registry.gitlab.com со стороны сети я в итоге наладил (это решалось на сетевом уровне). Но образы все равно не тянулись — и сообщение об ошибке указало на причину:

failed to pull ...: 403 Forbidden
... from GET https://cdn.registry.gitlab-static.net/.../blobs/...

Оказалось, что манифест образа и его слои отдаются с разных хостов: API-хост registry.gitlab.com отвечает за «где лежит образ», а собственно слои — за подписанными ссылками на cdn.registry.gitlab-static.net. Я открыл доступ к первому, а второй остался недоступен — и docker pull доходил до скачивания слоев и падал.

Вывод, который я унес: «реестр недоступен» — это часто не один адрес. Когда docker pull валится на скачивании слоев, первым делом смотри, с какого именно хоста идет запрос за слоями, а не только пингуй основной адрес реестра.

Усиление защиты systemd: двигай метрику, а не ощущение

Пока чинил релиз, заодно довел до ума юнит systemd. Удобно, что у systemd есть встроенная оценка «насколько служба опасна» — systemd-analyze security, шкала от 0 (паранойя) до 10 (все открыто). Стартовое значение моего юнита было 8.3 — «EXPOSED». После осмысленного закручивания гаек вышло 1.9 — «OK».

Что закрутил

Зачем

CapabilityBoundingSet= (пусто)

по умолчанию демону не нужна ни одна привилегия-capability

SystemCallFilter=@system-service

разрешить только «нормальный» для службы набор системных вызовов

ProtectSystem=strict, ProtectProc=invisible

файловая система только для чтения, чужие процессы не видны

RestrictAddressFamilies, RestrictNamespaces

только нужные виды сокетов, запрет создания пространств имен

MemoryDenyWriteExecute, LockPersonality, NoNewPrivileges

стандартная «гигиена» против целого класса трюков

Что закрыть нельзя — и почему: PrivateNetwork (демону нужна сеть для TCP-транспорта), PrivateDevices (символьная ссылка должна жить в настоящем /dev), запуск от root (нужен для создания узла в /dev и для самого сброса привилегий). Я не убирал то, что демону реально нужно, а остальное закрыл — и проверил живьем, что служба при этом нормально создает порт и работает.

Мораль та же, что и со всем релизом: если у проблемы есть числовая метрика — двигай метрику, а не «ощущение безопасности».

Каждый баг — отдельный тест

Отдельная практика, которая хорошо легла на всю эту историю: каждый пойманный баг закрывается регрессионным тестом, который воспроизводит именно ту поломку и покраснел бы, если фикс откатить. Тот самый псевдотерминал из первой статьи, который после закрытия порта грузил ядро на 100%, — это bug_0001 с тестом, который я специально проверил «на красноту», временно убрав фикс. Грабли из CI — это не только «починил и забыл», это еще и повод дописать проверку, чтобы то же самое не вернулось тихо.

Что бы я сделал иначе

  • Включал бы CI с первого дня, а не перед релизом. Тогда грабли приходили бы по одной и в контексте свежих изменений, а не все сразу в день выпуска.

  • Гонял бы матрицу версий с самого начала. Минимальный CMake, старый GCC, старый Doxygen — это не формальность из README, а реальная среда, в которой код должен собираться.

  • Воспроизводил бы окружение локально сразу. Поставить себе ровно тот CMake 3.22 и тот GCC 11, что на раннере, — самый быстрый способ чинить «версионные» грабли пачкой.

  • Помнил бы, что сеть раннера — часть окружения. Доступность реестров, CDN, артефактов зависит от того, откуда раннер ходит в интернет, а слои образа могут жить не там, где манифест.

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

Если вы попали сюда мимо первой статьи, а интересно, что у этого демона внутри (псевдотерминал, который грузил ядро на 100%, однопоточный epoll-цикл и эмуляция RS485), — это в «COM-порт из ничего: PTY, epoll и немного RS485-боли».

Исходники открыты, можно форкать и заводить задачи: https://gitlab.com/trgv/vspd. Вопросы и истории своих «локально-зеленых» грабель — в комментарии.