Привет, Хабр! Если интересно проанализировать недостатки инструмента inotify с позиции решения ИБ-задачи контроля целостности (именно самого инструмента в его первозданном виде), то добро пожаловать под кат!
Введение
Давайте поговорим о такой задаче информационной безопасности, как контроль целостности объектов файловой системы.
Какие инструменты контроля целостности в Linux приходят сразу на ум?
Из перечисленных инструментов aide, afick и tripwire работают по принципу «редкого» мониторинга, а wazuh, ossec — постоянного (более подробно эти понятия раскроются дальше).
В этой статье лишь немного затронем тему принципа работы «редкого» мониторинга, в большей степени поговорив о проблемах, возникающих при работе с inotify, который положен в основу постоянного мониторинга wazuh/ossec.
Мы не будем досконально рассматривать сам механизм ядра Linux, хорошего материала на эту тему достаточно, в том числе и на Хабре. Также не станем проводить гл��бокий анализ работы прекрасных утилит inotify-tools или incron. Речь пойдет именно про сам inotify в его первозданном виде без каких-либо обвязок и библиотек. Нам важно понять, с какими проблемами можно столкнуться в случае проектирования ПО, использующего сам inotify. Что касается inotify-tools, то здесь мы посмотрим на то, закрывают ли утилиты данного пакета описанные проблемы.
«Редкий» мониторинг
Первые три утилиты (AIDE, AFICK и tripwire) работают по принципу контрольного снимка. Сначала делается дамп состояния объектов файловой системы, на его основе составляется референсная БД.

Эти три инструмента — отличная и очень крутая вещь, когда ваша бизнес-логика подразумевает относительно редкую проверку целостности.
Недостаток в том, что обнаружение нарушения может быть выявлено гораздо позже факта инцидента. Например, вы выставили таймер проверки — 12 часов, в 00:00 и в 12:00 соответственно, а злоумышленник / сотрудник с шаловливыми ручками нарушил целостность очень важного файла в 0:37. Выходит, что вы узнаете о нарушении только спустя 11 часов 23 минуты. За это время много плохого может произойти.
Это можно обыграть, написав дополнительную логику проверки БД, скажем, раз в секунду. Однако в случаях, когда проверяемые объекты файловой системы довольно «жирные», такая дополнительная логика сильно нагрузит ПК.
Чтобы не быть голословными, проанализируем, как aide справится с домашней директорией, в которой целых 1 958 278 объектов (порядка двух миллионов).
Составим простенький файл конфигурации:
Файл конфигурации aide
# Database configuration database_in=file:/home/agirre/Work/aide_example/aide.db database_out=file:/home/agirre/Work/aide_example/aide.db.new database_new=file:/home/agirre/Work/aide_example/aide.db.new # Base rule BASIC = sha256+ftype+p+u+g # Directory for scan /home/agirre BASIC # Exclude cache and tmp files !/home/agirre/\.cache !/home/agirre/tmp
Запустим инициализацию базы данных:
sudo aide --cofnig=/home/agirre/Work/aide_example/aide.conf --init
Уже на начальном этапе работы с aide мы можем заметить, что процесс создания БД довольно трудоёмкий (целых 29 минут):

Теперь же напишем workaround, упомянутый выше, чтобы иметь функциональность непрерывного контро��я целостности. Дополнительно включим логирование потребления CPU и памяти, чтобы собрать статистику.
Workaround
#!/usr/bin/bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" while true do echo "Запуск проверки aide в $(date)" # Запускаем aide и собираем статистику одновременно aide --config="$SCRIPT_DIR/aide.conf" --check & aide_pid=$! # Собираем статистику пока работает aide pidstat -r -p "$aide_pid" 1 > "$SCRIPT_DIR/mem_stat_$aide_pid.txt" 2>/dev/null & mem_pid=$! pidstat -p "$aide_pid" 1 > "$SCRIPT_DIR/cpu_stat_$aide_pid.txt" 2>/dev/null & cpu_pid=$! # Ждем завершения aide wait "$aide_pid" # Останавливаем сбор статистики kill "$mem_pid" 2>/dev/null kill "$cpu_pid" 2>/dev/null echo "Проверка завершена, следующая через 2 секунды..." sleep 2 done
Результаты 12-часового запуска:

Как мы можем заметить, функции MEM(t) и CPU(t) периодические, примерное значение периода составляет 30 минут. Кстати говоря, это среднее время работы сканирования нашей тестовой директории с почти двумя миллионами объектов файловой системы.
Посмотрим более детально на то, как выглядят графики для одного запуска:

Потребление памяти растёт первые 10 минут, после чего следующие 20 минут остаётся на прежнем уровне. При этом участков роста два: первый более медленный и сопровождающийся постоянным потреблением CPU 100%, второй — более быстрый и сопровождающийся скачками CPU аж до 200+%. После выхода на плато по памяти (12%) идёт снижение потребления CPU (тренд 65%).
Рискну предположить, что сначала aide делает текущий снимок файловой системы, составляя новую БД, после чего производит сравнение со старой по контрольным суммам.
На ПК, на котором проводились замеры, ОЗУ составляет 16 Гб, то есть 12% — это 1.92 Гб. Ясно, что такое огромное потребление как ОЗУ, так и процессорной мощности не укладывается в реалии ПО для информационной безопасности. Недостаток «редкого» мониторинга не так просто обыграть.
Конечно, кто-то может возразить, что пример был подобран не самый лёгкий, но такова настоящая потребность бизнеса. И даже больше. В нашей практике крайне часто возникают требования, в которых заказчик хочет осуществлять постоянный мониторинг абсолютно всей файловой системы.
Постоянный мониторинг
Крутыми инструментами в приведенном в начале статьи списке являются wazuh и OSSEC. Плюс этого ПО в том, что оно умеет отслеживать изменения файловой системы в режиме реального времени.
Речь идет про постоянный мониторинг, когда мы подписываемся на определенные события файловой системы, получая уведомления о том, что они произошли.
Но как именно реализован постоянный мониторинг?
И wasuh, и OSSEC «под капотом» используют механизм inotify (и не только его).
Если собрать wazuh с включенным флагом INOTIFY_ENABLED (Makefile), то будет скомпилирован код с соответствующей логикой (один из файлов).
Аналогичная ситуация с OSSEC. Включив флаг INOTIFY_ENABLED (Makefile), добавим логику inotify в сборку (логика). Кстати говоря, wasuh — fork OSSEC.
Итак, наконец-то мы добрались до inotify.
inotify предоставляет удобное kernel-API, позволяющее мониторить события файловой системы в режиме реального времени. Ключевое отличие — нам не нужно крутить бесконечный цикл постоянного сканирования, нагружая ЦП. Уведомление о том, что событие произошло, придёт к нам в user space из kernel space.

Интерфейс прост, всего три функции:
int inotify_init (void); // инициализация подписки int inotify_init1 (int __flags); // (версия с флагами) int inotify_add_watch (int __fd, const char *__name, uint32_t __mask); // добавить путь ФС в подписку int inotify_rm_watch (int __fd, int __wd); // убрать путь ФС из подписки
Говорим о проблемах
Для рассмотрения проблем, возникающих при использовании inotify, информации достаточно. Прежде чем идти дальше, уделим некоторое внимание тестовому приложению, с помощью которого будем проводить эксперименты.
Тестовое ПО
Для экспериментов возьмем официальный пример отсюда.
Немного подредактируем некоторые вещи:
поменяем флаг в функции
inotify_add_watch; в примере стоитIN_ALL_EVENTS, нас же будут интересоватьIN_MODIFY,IN_ATTRIB,IN_DELETE_SELF,IN_MOVE_SELF(эти флаги присущи как файлам, так и директориям) иIN_CREATE,IN_DELETE,IN_MOVED_FROM,IN_MOVED_TO(эти же имеют смысл только для директорий);заведём статический контейнер
WATCH_DESCRIPTORS, в который будем складывать watch descriptor и соответствующий ему путь к объекту файловой системы;модифицируем функцию
displayInotifyEventтак, чтобы она выводила чуть больше информации;добавим ряд compile-зависимых функциональных особенностей, применение которых будет раскрыто по ходу статьи.
Результат:
Код demo-программы
Скрытый текст
#include <sys/inotify.h> #include <limits.h> #include <unistd.h> #include <sys/stat.h> #include <errno.h> #include <unordered_map> #include <string> #include <cstring> #include <filesystem> #include <ctime> #include <thread> #include <cmath> #define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1)) namespace fs = std::filesystem; static std::unordered_map<int, std::string> WATCH_DESCRIPTORS; static void /* Display information from inotify_event structure */ displayInotifyEvent(inotify_event *i) { auto it = WATCH_DESCRIPTORS.find(i->wd); if (it == WATCH_DESCRIPTORS.end()) { return; } printf(" filesystem object = %s; ", it->second.c_str()); if (i->cookie > 0) printf("cookie = %4d; ", i->cookie); printf("event = "); if (i->mask & IN_ATTRIB) { printf("attributes change "); if (i->len != 0) { printf("(inner item %.*s) ", i->len, i->name); } } if (i->mask & IN_CREATE) { printf("creation of inner element %.*s ", i->len, i->name); const fs::path fullPath = fs::path(it->second) / i->name; struct stat fileStat; if (stat(fullPath.c_str(), &fileStat) == 0) { printf("with inode %lu ", fileStat.st_ino); } } if (i->mask & IN_DELETE) printf("deletion of inner element %.*s ", i->len, i->name); if (i->mask & IN_DELETE_SELF) { printf("self deletion "); } if (i->mask & IN_MODIFY) { printf("modification "); if (i->len != 0) { printf("(inner item %.*s) ", i->len, i->name); } } if (i->mask & IN_MOVE_SELF) { printf("self movement "); } if (i->mask & IN_MOVED_FROM) { printf("moved %.*s from ", i->len, i->name); } if (i->mask & IN_MOVED_TO) { printf("moved %.*s to ", i->len, i->name); } printf("\n"); } static void displayWatchesCnt() { std::time_t now = time(nullptr); while (true) { double diff = difftime(time(nullptr), now); if (std::fabs(diff - 60.0) < std::numeric_limits<double>::epsilon()) { printf("COUNT IS %ld\n", WATCH_DESCRIPTORS.size()); now = time(nullptr); } } } int main(int argc, char *argv[]) { char buf[BUF_LEN] __attribute__((aligned(8))); ssize_t numRead; char *p; struct inotify_event *event; #ifndef RECURSIVE_HUGE_TEST if (argc < 2 || strcmp(argv[1], "--help") == 0) { printf("Error: incorrect usage\n"); exit(EXIT_FAILURE); } #endif #ifdef SLOWNESS_TEST if (argc != 2) { printf("Error: incorrect SLOWNESS_TEST usage\n"); exit(EXIT_FAILURE); } #endif const int inotifyFd = inotify_init(); /* Create inotify instance */ if (inotifyFd == -1) { printf("Error while initing: %s\n", strerror(errno)); exit(EXIT_FAILURE); } /* For each command-line argument, add a watch for events */ #ifndef RECURSIVE_HUGE_TEST for (int j = 1; j < argc; j++) { int wd = inotify_add_watch(inotifyFd, argv[j], IN_MODIFY | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (wd == -1) { exit(EXIT_FAILURE); } printf("Watching %s using wd %d\n", argv[j], wd); WATCH_DESCRIPTORS[wd] = argv[j]; } #else int cnt = 0; for (const auto &entry : fs::recursive_directory_iterator("/home")) { if (!fs::exists(entry.path())) { continue; } int wd = inotify_add_watch(inotifyFd, entry.path().c_str(), IN_MODIFY | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (wd == -1) { if (errno == 28) { printf("Error: %s on count %d\n", strerror(errno), cnt); } else { printf("Unexpected error: %s\n", strerror(errno)); } exit(EXIT_FAILURE); } ++cnt; } #endif std::thread backgroundTh(displayWatchesCnt); backgroundTh.detach(); for (;;) { /* Read events forever */ numRead = read(inotifyFd, buf, BUF_LEN); if (numRead == 0) { exit(EXIT_FAILURE); } if (numRead == -1) { exit(EXIT_FAILURE); } /* Process all of the events in buffer returned by read() */ for (p = buf; p < buf + numRead; p += sizeof(inotify_event) + event->len) { event = (inotify_event *)p; #ifndef SLOWNESS_TEST displayInotifyEvent(event); #else if (!(event->mask & IN_CREATE)) { continue; } if (event->len == 0) { continue; } const std::string &parentDirPath = WATCH_DESCRIPTORS[event->wd]; const fs::path innerPath = fs::path(parentDirPath) / event->name; int innerWd = inotify_add_watch(inotifyFd, innerPath.c_str(), IN_MODIFY | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (innerWd == -1) { printf("Unexpected error in SLOWNESS_TEST: %s\n", strerror(errno)); printf("%s\n", innerPath.c_str()); exit(EXIT_FAILURE); } WATCH_DESCRIPTORS[innerWd] = innerPath.string(); #endif } } exit(EXIT_SUCCESS); }
Какие проблемы есть при работе с регулярными файлами?
Достаточно ли метаданных предоставляет inotify с точки зрения ИБ?
Нет, в ходе проведения расследований инцидентов безопасности метаданных недостаточно.
Событие inotify описывается структурой inotify_event:
/* Structure describing an inotify event. */ struct inotify_event { int wd; /* Watch descriptor. */ uint32_t mask; /* Watch mask. */ uint32_t cookie; /* Cookie to synchronize two events. */ uint32_t len; /* Length (including NULs) of name. */ char name __flexarr; /* Name. */ };
Wd (watch descriptor) — это уникальный целочисленный идентификатор, который характеризует отслеживаемый файл с позиции inotify, mask — маска, указывающая на то, какие действия произошли (изменение, модификация атрибутов и т.д.), cookie нужны для синхронизации связанных между собой событий IN_MOVED_FROM и IN_MOVED_TO, len — длина поля name.
Этих атрибутов не хватит, чтобы понять, какая именно программа (какой процесс ОС) послужила причиной возникновения события, а также нет возможности узнать, какой именно пользователь нарушил целостность. Офицер безопасности узнает, что с конкретным файлом произошло определенное действие. Но в результате чего и кто виновник — это останется неизвестным.
Не слишком ли много событий генерирует inotify?
Во время использования инструмента возникает проблема, которая сказывается на стороне, обрабатывающей события безопасности, но не генерирующей. Дело в том, что вместо одного ожидаемого события можно получить сразу несколько. Этот недостаток не так очевиден, если рассматривать само ED-приложение (endpoint detection). Но если подойти со стороны SIEM-системы, у которой сотни (или даже тысячи) ED-агентов в качестве источников, то последствия очевидны: резкое увеличение нагрузки на модули корреляции и агрегации.
Cделаем следующее. Откроем подконтрольный файл в редакторе и изменим его. В условиях тестовой программы ожидается всего одно событие (модификация, то есть IN_MODIFY).
Видим же вовсе не одно событие:

./build/demo-inotify test.txt Watching test.txt using wd 1 filesystem object = test.txt; event = attributes change filesystem object = test.txt; event = self deletion filesystem object = test.txt; event =
Причина кроется не совсем в inotify, а в механизмах редактирования файлов со стороны ОС и приложений-редакторов. Чтобы понять, что происходит «под капотом», запустим наше тестовое ПО с учётом отслеживания директории, внутри которой модифицируется тестовый файл:

./build/demo-inotify . Watching . using wd 1 filesystem object = .; event = creation of inner element .goutputstream-0VSKJ3 with inode 11403307 filesystem object = .; event = attributes change (inner item .goutputstream-0VSKJ3) filesystem object = .; event = modification (inner item .goutputstream-0VSKJ3) filesystem object = .; cookie = 65079; event = moved .goutputstream-0VSKJ3 from filesystem object = .; cookie = 65079; event = moved test.txt to
Наблюдаем ряд последовательных событий:
Создание некоторого временного скрытого элемента "
.goutputstream-0VSKJ3"Изменение его прав доступа
Его модификация
Перемещение "
.goutputstream-0VSKJ3" (from)--> "test.txt" (to)
Текстовый редактор создаёт временный скрытый файл, меняет его дискреционные атрибуты на такие же, как и test.txt, копирует изменённое содержимое test.txt в него и сохраняет под тем же именем.
Так, всего одно событие разворачивается в 4.
Если пользователь удалит контролируемый файл, то продолжится ли его мониторинг?
Не продолжится. Данная проблема напрямую ассоциируется с бизнес-логикой приложения, так как зачастую клиент / заказчик интуитивно подразумевает, что программа занимается мониторингом объектов, описанных в конфигурации именно полным путём в файловой системе. Рядового пользователя не интересует, что такое inode, файловый дескриптор и т.д.
Суть проблемы в том, что после удаления ранее контролируемого объекта его мониторинг прекращается. Повторное создание не приводит к регистрации watch descriptor-а в inotify.
Возьмём подконтрольный файл, внесём в него ряд изменений, после чего удалим. И изменения, и удаление будут отображены:
./build/demo-inotify test.txt Watching test.txt using wd 1 filesystem object = test.txt; event = modification filesystem object = test.txt; event = modification filesystem object = test.txt; event = self deletion
Далее мы создадим данный файл снова под тем же именем и ровно в той же директории. Посмотрим, будут ли отображаться изменения теперь:

После удаления наблюдается полное отсутствие каких-либо событий вообще.
Почему так происходит?
Гипотеза: inotify оперирует inode объекта, а не его путём. Если удалить последнюю жёсткую ссылку на inode (что в примере и происходит), то ОС удаляет последнее имя объекта, что ссылалось на inode (то есть наш путь), и, если более никакая другая программа не использует файл (нет открытых дескрипторов), inode возвращается в пул свободных inodes. Далее при создании файла по точно такому же пути имеем уже другое значение inode (в теории).
Посмотрим на это, предварительно запомнив значение inode:

Как мы можем видеть, гипотеза опровергается (значения inode до удаления и после одинаковые), хотя при изменении файла через символическую ссылку изменения детектируются, что наталкивает на мысль о том, что inotify оперирует не путём объекта:

Внесение изменений в link.txt тоже приводит к возникновению события, а link.txt — ссылка на test.txt, поэтому этот вопрос остается открытым.
Вторая гипотеза: inotify автоматически удаляет watch descriptor при удалении связанного с ним объекта.
В данном вопросе требуются ваши знания, дорогие читатели, быть может, кто-то знает верный ответ?
Какие есть проблемы при работе с директориями?
Что с рекурсивностью?
Её нет. Все события, возникающие в контролируемой директории непосредственно, детектируются, в то время как всё, что происходит более чем на 1 уровень глубже, игнорируется.
Рассмотрим структуру тестовой директории dir:

Demo-приложение будем запускать с данной директорией в качестве аргумента, наблюдая за её целостностью.
Сначала создадим какой-либо объект внутри самой test_dir, а затем — внутри inner_dir:

Мы наблюдаем события, происходящие непосредственно в самой dir, но всё, что происходит уже на один уровень глубже — нет. Ни одно из созданий файла внутри inner_dir в логах не отобразилось.
А если написать логику рекурсивности самостоятельно, то будут ли ограничения на количество отслеживаемых объектов?
Да, важно понимать, что после самостоятельной реализации рекурсивности вы всё равно упретесь в ограничения.
Модифицируем demo-программу так, чтобы она рекурсивно обходила директорию /home, добавляя каждый объект в группу отслеживания. Для учета этой логики достаточно просто скомпилировать с флагом RECURSIVE_HUGE_TEST:
Модифицированная часть программы
#ifndef RECURSIVE_HUGE_TEST for (j = 1; j < argc; j++) { wd = inotify_add_watch(inotifyFd, argv[j], IN_MODIFY | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (wd == -1) { exit(EXIT_FAILURE); } printf("Watching %s using wd %d\n", argv[j], wd); WATCH_DESCRIPTORS[wd] = argv[j]; } #else int cnt = 0; for (const auto &entry : fs::recursive_directory_iterator("/home")) { if (!fs::exists(entry.path())) { continue; } wd = inotify_add_watch(inotifyFd, entry.path().c_str(), IN_MODIFY | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF | IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (wd == -1) { if (errno == 28) { printf("Error: %s on count %d\n", strerror(errno), cnt); } else { printf("Unexpected error: %s\n", strerror(errno)); } exit(EXIT_FAILURE); } ++cnt; } #endif
Всего элементов в /home чуть более двух миллионов:
sudo find /home/ | wc -l 2061369
В результате программа завершила свою работу, не успев начаться:
./build/demo-inotify Error: No space left on device on count 64986
No space left on device? Странно, вроде бы дискового пространства на ПК более чем достаточно. Замечу, что число 64986 у меня повторяется из раза в раз. Истинная причина — установленные лимиты.
У inotify есть три ключевых ограничения:
max_user_watches — максимальное количество наблюдаемых объектов, которое может создать пользователь (т.е. число watch descriptor-ов; то, что создаётся функцией
inotify_add_watch);max_user_instances — максимальное количество файловых дескрипторов inotify (то, что создаётся функцией
inotify_init);max_queued_events — максимальное число событий, которое может вместить в себя очередь, приуроченная к inotify-дескриптору.
Значения этих ограничений находятся в соответствующих файлах директории /proc/sys/fs/inotify/:

Увеличить лимит можно с помощью команды sysctl fs.inotify.max_user_watches=1000000:
# увеличить лимит sudo sysctl fs.inotify.max_user_watches=1000000 # проверить значение sudo cat /proc/sys/fs/inotify/max_user_watches
После увеличения программа прекращает свою работу уже на большем количестве watch descriptor-ов (правда, использование CPU при этом оказалось довольно большим):
./build/demo-inotifyError: No space left on device on count 1002043

Если я увеличиваю лимиты, то закрываю ли я все проблемы? Или остается что-то ещё?
Во-первых, увеличение лимитов приведет к росту потребления CPU. Во-вторых, даже после этого у inotify ещё остаются проблемы. Заключительная проблема, которую хотелось бы отметить — медлительность или «проскальзывание». Она возникает тогда, когда пользователь копирует директорию внушительного размера в папку, что находится под контролем целостности.
Логику demo-программы поменяем так, чтобы каждый новый добавленный объект тоже попадал под контроль (флаг компиляции SLOWNESS_TEST).
Допустим, у нас имеется директория под контролем целостности. Мы берём объёмную папку и копируем её внутрь. Предварительно не забудем увеличить лимиты, а тестовую объёмную папку наполним большим количеством объектов:
# увеличиваем лимит перед запуском sudo sysctl fs.inotify.max_user_watches=1000000 # проверяем количество элементов объемной папки find huge_dir_158318/ | wc -l
Время копирования на моем ПК составляет примерно 55 секунд, поэтому пусть раз в минуту программа выводит в консоль размер контейнера WATCH_DESCRIPTORS.
Компилируем нашу программу в Release mode (все оптимизации + высокая производительность). Запускаем и смотрим:

Финальное число дескрипторов отслеживания равно 157956, когда ожидали — 158138 + 1 (на директорию самого верхнего уровня).
Выходит, что «проскользнуло» порядка двухсот элементов, за целостностью которых мы не следим. Брешь с точки зрения безопасности.
Выводы
Итак, мы рассмотрели недостатки инструмента inotify, которые классифицировали по двум признакам:
при работе с файлами
при работе с директориями.
Неполнота метаданных: мы никак не можем повлиять на то, чтобы inotify стал более выразителен (разве что только сделать свой вклад в ядро LInux);
Избыточность событий: следует принимать во внимание устройство работы различных текстовых редакторов с файлами;
Проблема идентификации: удаление файла приводит к его исключению из группы мониторинга, а повторное создание по тому же пути не подразумевает, что он снова находится под контролем целостности;
Отсутствие рекурсивности: сам по себе inotify не отслеживает всё дерево директории, а работает лишь с первым уровнем глубины;
Ограничения по количеству отслеживаемых объектов: в случае решения проблемы рекурсивности возникает проблема лимитов на watch descriptor-ы;
Медлительность (или «проскальзывание»): к сожалению, может возникнуть ситуация, во время которой часть объектов файловой системы «проскочит» мимо inotify-обработчика.
Механизм inotify — это очень классная вещь, но она не без недостатков. Следует учитывать их при проектировании системы ИБ, в основу мониторинга которой заложен inotify непосредственно.
Быть может, есть какие-то альтернативные решения, внедрение в проект которых позволит избавиться от перечисленных проблем?
Неполнота метаданных
Если крайне важна информация о том, какая программа привела к нарушению целостности и какой именно пользователь это сделал, то можно обратить своё внимание на fanotify. Это более обширный и сложный альтернативный инструмент, однако его минус в сложности конфигурации.
Использование утилит inotifywatch/inotifywait пакета inotify-tools проблему не решит:
Пример использования inotifywait
inotifywait -m -e modify -e moved_to -e moved_from -e move -e move_self -e delete_self test.txt Setting up watches. Watches established. test.txt MODIFY test.txt MODIFY test.txt MODIFY test.txt MOVE_SELF test.txt MOVE_SELF test.txt DELETE_SELF
Пример использования inotifywatch
inotifywatch -v -e modify -e moved_to -e moved_from -e move -e move_self -e delete_self test.txt Establishing watches... Total of 1 watches. Finished establishing watches, now collecting statistics. ^Ctotal modify move_self delete_self filename 7 3 2 1 test.txt
Избыточность событий
Проблема здесь в том, как именно различные программы взаимодействую с файловой системой, создавая временные файлы, где на самом деле происходит модификация. Избыточность событий inotify — это следствие.
Возможным решением проблемы может быть фильтрация событий по времени их возникновения (параметр cookie событий класса IN_MOVED_TO/IN_MOVED_FROM, а также самописная логика агрегации на стороне ED-приложения).
Так же, как и в предыдущем случае, использование утилит пакета inotify-tools не помогает избавиться от проблемы:

Проблема идентификации
Ни сам инструмент inotify, ни утилиты, базирующиеся на нём, не решают проблему. Если посмотреть на ситуацию с точки зрения операционной системы, то её нельзя назвать недостатком, так как файловый объект до удаления и после его повторного создания — это две разных сущности, хоть они и обладают одним и тем же путём. Если же зайти со стороны пользователя и его бизнес-требований, то это можно считать недочётом.
В принципе, исключение файла из группы мониторинга в ходе его удаления и повторного создания можно обойти, если следить за директорией, внутри которой он находится.
Отсутствие рекурсивности
Здесь имеется ряд альтернатив. Можно либо добавить необходимую поддержку рекурсивности самостоятельно, либо использовать fanotify с его маской FAN_MARK_MOUNT (её минус в том, что она работает только для точек монтирования, то есть не для любой директории), либо же взять готовую реализацию из inotify-tools.
Ограничения по количеству отслеживаемых объектов
Решением «в лоб» здесь будет увеличение лимитов, минус такого подхода — рост потребления CPU. Другой же альтернативой будет мониторинг только директорий (таким образом watch descriptor-ы будут кратно меньше, а события, связанные с файлами первого уровня вложенности, будут успешно детектироваться). Кстати, именно такую оптимизацию можно найти в коде inotify-tools (функция inotifytools_watch_recursively_with_exclude). Минус здесь — наличие лишних событий по тем файлам, что расположены в папках, но не находятся в зоне интереса пользователя (их нужно будет самостоятельно фильтровать на уровне ПО).
Медлительность
Одним из альтернативных решений здесь может стать периодическое сканирование подконтрольной директории, в ходе которого «проскользнувшие» объекты будут регистрироваться в inotify.
