Дружимо Auditd і Контейнери

Вступ

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

auditd-meme

Контекст Через 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. Проте, є й недоліки:

Контекст Через 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 в допомогу), який:

  1. В реальному часі читає Auditd події з файлу та парсить поле pid
  2. Через /proc/pid/cgroup знаходить ідентифікатор контейнера
  3. Отримує контекст контейнера через Docker/Kubernetes CLI/API
  4. Кешує та додає отриманий контекст в події Auditd

Усі container-aware інструменти працюють за схожим принципом - збирають логи чи системні виклики як і Auditd, а наостанок додають контекст по контейнерах чи хмарі. Приклади: Fluentbit, Elastic, Falco.

У Підсумку

З труднощами, але Auditd все ж можна подружити з контейнерами, якщо й не для повноцінного моніторингу, то хоч щоб зрозуміти як працює контейнеризація. Я наполегливо раджу спробувати відтворити цей пост у себе на машині для навчальних цілей, проте не менш наполегливо раджу не використовувати Auditd для повноцінного моніторингу контейнеризованих середовищ - є чимало зручніших альтернатив, як от Falco :)