Дружимо Auditd і Контейнери
Вступ
Коли йде розмова за моніторинг системних викликів Linux - першим на думку спадає Auditd. Проте, коли уточнюють що моніторинг має бути container-aware, про Auditd вже ні слова. Чому ж такі інструменти як Wiz, Lacework, чи Falco вміють розрізняти події з різних контейнерів, а Auditd ні? У цьому пості я спробую пояснити як все працює зсередини, та чи можна подружити Auditd з контейнерами.

Контекст Через Inode
Уявіть сценарій - на хості стоїть Auditd, а також десять Docker контейнерів. В один момент, в логах Auditd ви бачите команду sleep 500 (приклад нижче). Як взнати де виконана команда: на хості чи на одному з контейнерів?
root@host:~# ausearch -i -x sleep
type=PROCTITLE msg=audit(09/30/25 19:04:04.249:656): proctitle=sleep 500
type=PATH msg=audit(09/30/25 19:04:04.249:656): item=0 name=/usr/bin/sleep inode=566597 [...]
type=SYSCALL msg=audit(09/30/25 19:04:04.249:656): syscall=execve uid=root exe=/usr/bin/sleep [...]
Перший метод - це орієнтуватись на поле inode, унікальний ідентифікатор метадати файлу. В прикладі вище, Auditd вказує що 566597 є ідентифікатором файлу /usr/bin/sleep. Проте, це не зовсім так. Давайте власноруч перевіримо на який файл насправді вказує цей inode:
root@host:~# debugfs -R 'ncheck 566597' /dev/root # /dev/<диск>
Inode Pathname
566597 /var/lib/docker/overlay2/ee35a31834ef36[...]/diff/usr/bin/sleep
Inode 566597 насправді вказує на файл з /var/lib/docker/overlay2/, частини OverlayFS одного з контейнерів. Далі, по назві директорії можна визначити конкретний контейнер. Наприклад, скориставшись Docker CLI:
for container in $(docker ps --all --format '{{ .Names }}'); do
docker container inspect --format '{{.GraphDriver.Data }}' ${container} \
| grep ee35a31834ef36 | grep -oP 'ID:\K.{12}'
done
OUTPUT: 670c83568ef1
Маючи ідентифікатор контейнера, 670c83568ef1, залишається взнати його деталі через Docker CLI. Ось так, в декілька кроків, можна виявити що команду sleep запустив контейнер website, що працює на WordPress. Якби замість sleep були більш підозрілі команди, ви б вже знали який контейнер досліджувати далі.
root@host:~# docker container ls --all
CONTAINER ID IMAGE COMMAND NAMES
670c83568ef1 wordpress "docker-entrypoint.sh" website
15095865db9e prodapi "/start.sh" prodapi
Недоліки
Незалежно від платформи контейнеризації, як от Docker чи CRI-O/Kubernetes, цей метод має працювати і може бути корисним для форензики або моніторингу з Auditd. Проте, є й недоліки:
- Метод не спрацює якщо цільовий контейнер видалили
- Метод не спрацює якщо програму, як от /usr/bin/sleep, видалили
- Метод підходить лише для OverlayFS (який є де-факто стандартом)
- Команди можуть дещо відрізнятися залежно від середовища
Контекст Через Cgroups
У цій секції я опишу альтернативний метод, проте спочатку варто нагадати, що контейнери працюють завдяки namespaces та cgroups, двом технологіям що дозволяють гнучку ізоляцію та контроль ресурсів ОС. До прикладу, якщо на хості паралельно запустити команди sleep 30 і sleep 60, їх cgroups будуть збігатися, адже утворені процеси не ізольовані один від одного:
root@host:~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 6089 0.0 0.0 6112 1792 pts/1 S 20:39 0:00 sleep 30
root 6090 0.0 0.0 6112 1792 pts/1 S 20:39 0:00 sleep 60
[...]
root@host:~# cat /proc/6089/cgroup
0::/user.slice/user-1000.slice/session-2.scope
root@host:~# cat /proc/6090/cgroup
0::/user.slice/user-1000.slice/session-2.scope
А ось контейнери ізольовані один від одного, і тому cgroup буде унікальним для кожного контейнера. Ця властивість якраз стане нам в пригоді, тому повернімось до вищезгаданої Auditd події і детальніше розглянемо поле pid (Process ID):
root@host:~# ausearch -i -x sleep
type=PROCTITLE msg=audit(09/30/25 19:04:04.249:656): proctitle=sleep 500
type=PATH msg=audit(09/30/25 19:04:04.249:656): item=0 name=/usr/bin/sleep inode=566597 [...]
type=SYSCALL msg=audit(09/30/25 19:04:04.249:656): \
arch=x86_64 syscall=execve exit=0 \
ppid=5587 pid=5594 auid=unset uid=root tty=pts0 \
comm=sleep exe=/usr/bin/sleep subj=docker-default [...]
Маючи ідентифікатор процесу 5594, виводимо його cgroup через команду cat, а у виводі вже знаходимо ідентифікатор контейнера. У випадку Docker, це перші 12 символів після слова “docker-”, 670c83568ef1. Далі залишається скористатись Docker CLI та отримати контекст контейнера:
root@host:~# cat /proc/5594/cgroup
0::/system.slice/docker-670c83568ef1[...].scope
root@host:~# docker container ls --all
CONTAINER ID IMAGE COMMAND NAMES
670c83568ef1 wordpress "docker-entrypoint.s…" website
15095865db9e prodapi "/start.sh" prodapi
Недоліки
Описаний метод цікавий та зручний, проте має один великий недолік, він не підійде якщо процесу вже не існує. Якщо хакер запустив whoami, і вам потрібно взнати в якому саме контейнері - цей метод не допоможе оскільки процес whoami завершить роботу ще до того як ви відкриєте термінал.
Автоматичний Збір Контексту
Обидва методи збору контексту повністю залежать від стану контейнерів - чим довше ви чекаєте, тим менше контексту зможете зібрати. Тому, можна написати скрипт (ChatGPT в допомогу), який:
- В реальному часі читає Auditd події з файлу та парсить поле pid
- Через /proc/pid/cgroup знаходить ідентифікатор контейнера
- Отримує контекст контейнера через Docker/Kubernetes API
- Кешує та додає отриманий контекст в події Auditd
Усі container-aware інструменти працюють за схожим принципом - збирають логи чи системні виклики як і Auditd, а наостанок додають контекст по контейнерах чи хмарі. Приклади: Fluentbit, Elastic, Falco.
У Підсумку
З труднощами, але Auditd все ж можна подружити з контейнерами, якщо й не для повноцінного моніторингу, то хоч щоб зрозуміти як працює контейнеризація. Я наполегливо раджу спробувати відтворити цей пост у себе на машині для навчальних цілей, проте не менш наполегливо раджу не використовувати Auditd для повноцінного моніторингу контейнеризованих середовищ - є чимало зручніших альтернатив, як от Falco :)