От capabilities к AppArmor: что реально остановит атакующего в контейнере

от автора

Представьте себе обычный контейнер с веб-приложением. В нём есть уязвимость, злоумышленник получает возможность выполнять команды — и дальше начинается самое интересное: что именно его остановит?

Не в теории, а на практике. Не в изолированном примере «вот как работает seccomp», «вот что такое capabilities» и «вот зачем нужен AppArmor», а в одной и той же рабочей нагрузке, где все эти механизмы встречаются одновременно.

Последний год я много писал о примитивах безопасности Linux и о том, как они связаны с Kubernetes. Особенно часто — о том, как применять ограничения через securityContext, а затем усиливать их инструментами вроде Kyverno и KubeArmor, чтобы всё это можно было использовать не только в лабораторных примерах, но и в реальной среде.

Но сама по себе настройка механизма ещё не отвечает на главный вопрос: какую именно угрозу он закрывает и где заканчиваются его возможности?

В Linux, а значит и в Kubernetes, есть три механизма, на которых здесь стоит сосредоточиться:

  • capabilities;

  • seccomp;

  • LSM — например, AppArmor или SELinux.

Обычно их объясняют по отдельности. Иногда так же по отдельности и внедряют. Именно в этом месте часто появляются ложные ожидания: кажется, что если включить один механизм, он «закроет безопасность контейнера». На практике всё сложнее.

Capabilities определяют, что процессу в принципе разрешено делать. seccomp ограничивает то, как он взаимодействует с ядром через системные вызовы. LSM-политики оценивают уже само поведение в контексте конкретной рабочей нагрузки.

Это разные уровни, разные модели контроля и разные компромиссы. Если пытаться заставить один из них решать всё сразу, можно либо сломать приложение, либо оставить очевидные дыры.

Поэтому полезнее смотреть на них не как на взаимозаменяемые настройки, а как на несколько слоёв защиты. Каждый слой закрывает свою часть атаки. По отдельности их можно обойти или обойтись без них слишком широко. Вместе они начинают выглядеть уже не как набор разрозненных фич, а как реальная граница для скомпрометированного процесса.

В этой статье я возьму одну простую рабочую нагрузку и покажу, как на неё последовательно влияют capabilities, seccomp и AppArmor: где каждый механизм помогает, где бессилен и почему их имеет смысл использовать вместе. Это не рейтинг и не попытка выбрать «лучший» инструмент. Это попытка понять, как собрать из них работающую защиту.


Переосмысление

Capabilities обычно рассматривают отдельно, seccomp – отдельно, LSM – тоже отдельно. Для изучения это удобно, но использовать их предполагается не так.

В реальной рабочей нагрузке вы не разворачиваете «просто seccomp» в изоляции. Вы не полагаетесь только на capabilities. И LSM не закрывают всё магическим образом. На практике все эти механизмы работают в одном и том же месте:

  • один и тот же контейнер

  • один и тот же процесс

  • один и тот же путь выполнения

При этом они не перекрывают друг друга полностью и явно решают не одну и ту же задачу. Поэтому вместо вопроса «что делает каждый из них?» полезнее спрашивать, где именно применяется каждый механизм и какую часть проблемы он пытается контролировать.

Если смотреть на это так, начинает вырисовываться закономерность. Это не взаимозаменяемые механизмы. Они накладываются друг на друга и формируют поведение рабочей нагрузки. Один влияет на то, что ей разрешено делать, другой – на то, как она взаимодействует с системой, а третий – на то, какие действия действительно допустимы.

И что ещё важнее, каждый из них оставляет пробелы в разных местах. Именно поэтому разбор по отдельности быстро упирается в потолок. Самое интересное начинается, когда видно, как они выстраиваются вместе. Тогда они начинают выглядеть не как отдельные возможности, а как части одной общей границы.

Диаграмма

Если изобразить это схематично, получается уже не столько набор отдельных механизмов, сколько последовательность. Каждый уровень оценивает одно и то же действие по-своему.

Это уровни, которые оценивают одно и то же действие по-разному.

  • Capabilities определяют, что в принципе возможно.

  • seccomp контролирует, как используется система.

  • LSM решают, какое поведение действительно разрешено.

Они не заменяют друг друга. Они существуют именно потому, что остальных уровней недостаточно.

Начнём со скомпрометированной рабочей нагрузки

Для наглядности возьмём простой пример.

Предположим, у нас есть контейнер, в котором работает базовое веб-приложение. Ничего особенного: небольшой сервис, который принимает запросы и обрабатывает входные данные. Пример приложения можно найти на github.

Для этого примера мы намеренно оставляем всё простым: без запуска от непривилегированного пользователя, без дополнительного усиления защиты, только с фокусом на этих трёх механизмах.

В приложении есть уязвимость, и злоумышленник получает возможность выполнять команды внутри контейнера. В этот момент рабочая нагрузка делает ровно то, для чего была создана, просто не тем способом, который мы подразумевали.

И тогда возникает вопрос: что на самом деле останавливает такое поведение?

Capabilities: ров с водой

В нашем скомпрометированном контейнере злоумышленник может выполнять команды внутри рабочей нагрузки. На этом этапе контейнер не делает ничего «особенного».

Capabilities на верхнем уровне определяют, что этому процессу вообще разрешено делать. Даже по умолчанию контейнеры не запускаются с полным набором привилегий. Docker и Kubernetes уже отбрасывают большое количество capabilities и оставляют только сокращённый набор.

Это и есть ров с водой.

Теперь вернёмся к злоумышленнику. Он всё ещё может выполнять команды, исследовать файловую систему, читать доступные файлы и устанавливать исходящие сетевые соединения.

Проверим это.

До удаления capabilities

Из скомпрометированного контейнера злоумышленник может выполнять команды:

python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'# unshare -r -n bashunshare -r -n bashunshare: unshare failed: Operation not permitted# cat /etc/shadowcat /etc/shadowroot:*:20339:0:99999:7:::daemon:*:20339:0:99999:7:::bin:*:20339:0:99999:7:::sys:*:20339:0:99999:7:::sync:*:20339:0:99999:7:::

После удаления capabilities

После удаления capabilities вроде CAP_NET_RAW некоторые действия становятся невозможны. Вот обновлённый манифест Deployment:

apiVersion: apps/v1kind: Deploymentmetadata:  name: flask-appspec:  replicas: 1  selector:    matchLabels:      app: flask-app  template:    metadata:      labels:        app: flask-app    spec:      containers:        - name: flask          image: sfmatt/flask-vuln-demo          ports:            - containerPort: 5000          securityContext:            capabilities:              drop:                - NET_RAW

После применения нового манифеста и перезапуска Deployment:

# python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'Traceback (most recent call last):  File "<string>", line 1, in <module>  File "/usr/local/lib/python3.11/socket.py", line 232, in __init__    _socket.socket.__init__(self, family, type, proto, fileno)PermissionError: [Errno 1] Operation not permitted# unshare -r -n bashunshare -r -n bash# cat /etc/shadowcat /etc/shadowroot:*:20339:0:99999:7:::daemon:*:20339:0:99999:7:::bin:*:20339:0:99999:7:::sys:*:20339:0:99999:7:::sync:*:20339:0:99999:7:::

Теперь создание raw-сокета заблокировано, а остальные команды по-прежнему работают. Raw-сокеты позволяют низкоуровнево взаимодействовать с сетевым стеком. Для злоумышленника это может быть полезно, но обычному веб-приложению такое, как правило, не требуется.

Capabilities сокращают набор того, что в принципе возможно. Они убирают целые категории действий, но не управляют тем, как выполняется оставшееся поведение. Это видно по последним командам, которые всё ещё успешно отрабатывают.

seccomp: стена

Capabilities определяют, что возможно. seccomp контролирует, как используется система. Даже после ограничения capabilities каждое действие внутри контейнера всё равно проходит через ядро. seccomp фильтрует эти взаимодействия на уровне системных вызовов.

Это и есть стена.

Для seccomp не важно, что именно процесс пытается сделать. Важно только, как он это делает. Поэтому даже команды, которые выглядят допустимыми, могут завершаться ошибкой, если они опираются на запрещённые системные вызовы.

На этом этапе всё ещё доступны два пути:

# unshare -r -n bashunshare -r -n bash# cat /etc/shadowcat /etc/shadowroot:*:20339:0:99999:7:::daemon:*:20339:0:99999:7:::bin:*:20339:0:99999:7:::sys:*:20339:0:99999:7:::sync:*:20339:0:99999:7:::

Теперь применим seccomp.

После применения seccomp

После применения seccomp некоторые системные вызовы и сценарии их использования становятся недоступны. Вот обновлённый манифест Deployment:

apiVersion: apps/v1kind: Deploymentmetadata:  name: flask-appspec:  replicas: 1  selector:    matchLabels:      app: flask-app  template:    metadata:      labels:        app: flask-app    spec:      containers:        - name: flask          image: sfmatt/flask-vuln-demo          ports:            - containerPort: 5000          securityContext:            capabilities:              drop:                - NET_RAW            seccompProfile:              type: RuntimeDefault   # включает фильтрацию системных вызовов

И после применения нового манифеста:

# unshare -r -n bashunshare -r -n bashunshare: unshare failed: Operation not permitted# cat /etc/shadowcat /etc/shadowroot:*:20339:0:99999:7:::daemon:*:20339:0:99999:7:::bin:*:20339:0:99999:7:::sys:*:20339:0:99999:7:::sync:*:20339:0:99999:7:::

Теперь unshare заблокирован. Почему именно этот пример? unshare создаёт новые пространства имён и зависит от системных вызовов ядра, которые ограничивает seccomp.

Capabilities не перекрыли этот путь, а seccomp перекрыл. Однако оставшаяся команда всё ещё работает. Команды, которые опираются на разрешённые системные вызовы, по-прежнему будут успешно выполняться.

LSM: охрана

LSM определяют, какое поведение действительно разрешено. Даже после ограничения capabilities и фильтрации системных вызовов контейнер всё ещё может выполнять широкий набор «допустимых» действий. С точки зрения ядра эти действия совершенно нормальны.

И вот тут в дело вступают наши старые добрые LSM. AppArmor или SELinux оценивают поведение с учётом контекста. Их вопрос другой: не что процесс может сделать и не каким способом, а допустимо ли это действие для конкретной рабочей нагрузки.

Это и есть охрана.

На этом уровне мы уже не ограничиваем доступ к системе как таковой. Мы принудительно задаём, что допустимо для этой рабочей нагрузки.

На этом этапе остаётся один путь:

# cat /etc/shadowcat /etc/shadowroot:*:20339:0:99999:7:::daemon:*:20339:0:99999:7:::bin:*:20339:0:99999:7:::sys:*:20339:0:99999:7:::sync:*:20339:0:99999:7:::

Теперь снова вернёмся к злоумышленнику. Он всё ещё может выполнять команды и взаимодействовать с системой через разрешённые системные вызовы, но теперь эти действия оцениваются по политике безопасности.

После применения LSM

После применения политики LSM можно заблокировать даже те взаимодействия с системой, которые сами по себе выглядят допустимыми. В этом примере мы будем напрямую использовать AppArmor.

Сначала создаём профиль AppArmor на узле:

#include <tunables/global>profile flask-deny-shadow flags=(attach_disconnected,mediate_deleted) {  #include <abstractions/base>  file,  network,  capability,  deny /etc/shadow r,}

Загружаем профиль на узле:

sudo apparmor_parser -r flask-deny-shadow

Затем привязываем профиль к рабочей нагрузке с помощью appArmorProfile:

apiVersion: apps/v1kind: Deploymentmetadata:  name: flask-appspec:  replicas: 1  selector:    matchLabels:      app: flask-app  template:    metadata:      labels:        app: flask-app    spec:      containers:        - name: flask          image: sfmatt/flask-vuln-demo          ports:            - containerPort: 5000          securityContext:            capabilities:              drop:                - NET_RAW            seccompProfile:              type: RuntimeDefault            appArmorProfile:  type: Localhost  localhostProfile: flask-deny-shadow

После применения манифеста и перезапуска Deployment:

root@flask-app-687986cfdf-cschv:/app# cat /etc/shadowcat: /etc/shadow: Permission denied

На этом этапе команда завершается ошибкой не потому, что процессу не хватает capability, и не потому, что заблокирован системный вызов. Она завершается ошибкой потому, что такое поведение нарушает профиль AppArmor, заданный для рабочей нагрузки.

И вот у нас уже три перекрытых пути. Каждый уровень закрыл свой путь для атаки.

Чтобы проверить, насколько уверенно вы ориентируетесь в безопасности K8s, пройдите вступительный тест. Он поможет быстро увидеть сильные места и найти пробелы, которые стоит закрыть.

Визуализация уровней

Каждый уровень убирает отдельный путь. Capabilities блокируют создание raw-сокетов, seccomp блокирует создание пространства имён, а AppArmor блокирует доступ к файлу с чувствительными данными.

Подведем итог

Порядок здесь не главное. Можно выстроить эти уровни в другой последовательности и всё равно прийти к тому же результату. Так что, если хотите, можете начать со стены.

Важно другое: должны присутствовать все три уровня.

  • Capabilities убирают целые категории действий.

  • seccomp ограничивает то, как используется система.

  • LSM определяют, какое поведение разрешено.

По отдельности ни один из этих механизмов не решает задачу полностью. Вместе они справляются вполне неплохо. И, надеюсь, к этой троице я ещё какое-то время возвращаться не буду.

Тема контейнерной безопасности быстро упирается в практику: какие настройки действительно ограничивают атакующего, где остаются слабые места и как это переносится на Kubernetes-кластер.

В OTUS скоро пройдут два бесплатных урока по безопасности K8s. На них можно будет познакомиться с экспертами, задать вопросы по своим кейсам и заодно понять, насколько формат обучения подходит под ваши задачи.

  • 28 мая в 20:00. «Безопасность K8s: основные концепции и частые проблемы»
    Рассмотрим базовые механизмы безопасности Kubernetes и зоны, которые стоит проверять в первую очередь. Записаться

  • 18 июня в 20:00. «Kubernetes под прицелом: почему ваш кластер может взломать даже стажер и как этого избежать»
    Поговорим о типовых векторах атак, историях взломов и сценариях, где доступ к одному контейнеру становится проблемой для всего кластера. Записаться

Полный список бесплатных уроков мая смотрите в дайджесте.

ссылка на оригинал статьи https://habr.com/ru/articles/1039572/