Дружимо 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.s…" website
15095865db9e prodapi "/start.sh" prodapi
Недоліки
Незалежно від платформи контейнеризації, як от Docker чи CRI-O/Kubernetes, цей метод має працювати і може бути корисним для форензики або моніторингу з Auditd. Проте, є й недоліки:
- Метод не спрацює якщо цільовий контейнер видалили
- Метод не спрацює якщо програму, як от /usr/bin/sleep, видалили
- Метод підходить лише для OverlayFS (який є де-факто стандартом)
- Команди можуть дещо відрізнятися залежно від середовища
Контекст Через Cgroups
У цій секції я опишу альтернативний метод, проте спочатку варто нагадати, що контейнери працюють завдяки namespaces та cgroups, двом технологіям що дозволяють гнучку ізоляцію та контроль ресурсів ОС. До прикладу, якщо послідовно запустити команди sleep 300 і sleep 600, їх cgroup будуть збігатися, адже утворені процеси не ізольовані один від одного:
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 300
root 6090 0.0 0.0 6112 1792 pts/1 S 20:39 0:00 sleep 600
[...]
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, а у назві cgroup вже знаходимо ідентифікатор контейнера. У випадку 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 CLI/API
- Кешує та додає отриманий контекст в події Auditd
Усі container-aware інструменти працюють за схожим принципом - збирають логи чи системні виклики як і Auditd, а наостанок додають контекст по контейнерах чи хмарі. Приклади: Fluentbit, Elastic, Falco.
У Підсумку
З труднощами, але Auditd все ж можна подружити з контейнерами, якщо й не для повноцінного моніторингу, то хоч щоб зрозуміти як працює контейнеризація. Я наполегливо раджу спробувати відтворити цей пост у себе на машині для навчальних цілей, проте не менш наполегливо раджу не використовувати Auditd для повноцінного моніторингу контейнеризованих середовищ - є чимало зручніших альтернатив, як от Falco :)