Дружимо 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.sh"   website
15095865db9e   prodapi              "/start.sh"              prodapi

Недоліки

Незалежно від платформи контейнеризації, як от Docker чи CRI-O/Kubernetes, цей метод має працювати і може бути корисним для форензики або моніторингу з Auditd. Проте, є й недоліки:

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

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

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

У Підсумку

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