Хабрахабр

[Перевод] Seccomp в Kubernetes: 7 вещей, о которых надо знать с самого начала

Прим. перев.: Представляем вниманию перевод статьи старшего инженера по безопасности приложений британской компании ASOS.com. С ней он начинает цикл публикаций, посвящённых повышению безопасности в Kubernetes благодаря использованию seccomp. Если введение понравится читателям, мы последуем за автором и продолжим с его будущими материалами по этой теме.

В первой части я расскажу об основах и внутренних деталях реализации seccomp в Kubernetes. Эта статья — первая из серии публикаций о том, как создавать профили seccomp в духе SecDevOps, не прибегая к магии и колдовству.

Статья посвящена Secure Computing Mode, также известному как seccomp. Экосистема Kubernetes предлагает достаточное разнообразие способов по обеспечению безопасности и изоляции контейнеров. Его суть состоит в фильтрации системных вызовов, доступных для выполнения контейнерами.

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

Среда исполнения контейнера активирует их во время его запуска, чтобы ядро могло вести контроль за их исполнением. Профили seccomp определяют, какие системные вызовы должны быть разрешены или запрещены. Применение подобных профилей позволяет ограничить вектор атаки и сократить урон в случае, если какая-либо программа внутри контейнера (то есть ваши зависимости, или их зависимости) начнет делать то, что ей не разрешено.

Разбираемся с основами

Базовый профиль seccomp включает три элемента: defaultAction, architectures (или archMap) и syscalls:

]
}

(medium-basic-seccomp.json)

Чтобы упростить задачу, сосредоточимся на двух основных значениях, которые будут использоваться: defaultAction определяет судьбу по умолчанию любого системного вызова, не указанного в разделе syscalls.

  • SCMP_ACT_ERRNO — блокирует выполнение системного вызова,
  • SCMP_ACT_ALLOW — разрешает.

В разделе architectures перечисляются целевые архитектуры. Это важно, поскольку сам фильтр, применяемый на уровне ядра, зависит от идентификаторов системных вызовов, а не от их названий, прописанных в профиле. Перед применением среда исполнения контейнера сопоставит их с идентификаторами. Смысл в том, что системные вызовы могут иметь совершенно разные ID в зависимости от архитектуры системы. Например, системный вызов recvfrom (используется для получения информации от сокета) имеет ID = 64 в x64-системах и ID = 517 в x86. Здесь вы можете найти список всех системных вызовов для архитектур x86-x64.

Например, можно создать белый список, установив defaultAction на SCMP_ACT_ERRNO, а вызовам в секции syscalls присвоить SCMP_ACT_ALLOW. В секции syscalls перечисляются все системные вызовы и указывается, что с ними следует делать. Для чёрного списка следует поменять значения defaultAction и действия на противоположные. Тем самым вы разрешаете только вызовы, прописанные в разделе syscalls, и запрещаете все остальные.

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

1. AllowPrivilegeEscalation=false

В securityContext контейнера имеется параметр AllowPrivilegeEscalation. Если он установлен в false, контейнеры будут запускаться с установленным (on) битом no_new_priv. Смысл этого параметра очевиден из названия: он не позволяет контейнеру запускать новые процессы с привилегиями, бóльшими, чем имеются у него самого.

Таким образом, все системные вызовы, необходимые для запуска внутренних процессов среды исполнения (например, установка идентификаторов пользователя/группы, отбрасывание некоторых capabilities), должны быть разрешены в профиле. Побочным эффектом этого параметра, установленного в true (значение по умолчанию), является то, что runtime контейнера применяет профиль seccomp в самом начале процесса запуска.

Контейнеру, который выполняет банальное echo hi, потребуются следующие разрешения:

{ "defaultAction": "SCMP_ACT_ERRNO", "architectures": [ "SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32" ], "syscalls": [ { "names": [ "arch_prctl", "brk", "capget", "capset", "chdir", "close", "execve", "exit_group", "fstat", "fstatfs", "futex", "getdents64", "getppid", "lstat", "mprotect", "nanosleep", "newfstatat", "openat", "prctl", "read", "rt_sigaction", "statfs", "setgid", "setgroups", "setuid", "stat", "uname", "write" ], "action": "SCMP_ACT_ALLOW" } ]
}

(hi-pod-seccomp.json)

… вместо этих:

{ "defaultAction": "SCMP_ACT_ERRNO", "architectures": [ "SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32" ], "syscalls": [ { "names": [ "arch_prctl", "brk", "close", "execve", "exit_group", "futex", "mprotect", "nanosleep", "stat", "write" ], "action": "SCMP_ACT_ALLOW" } ]
}

(hi-container-seccomp.json)

Лично я избегал бы внесения в белый список следующих системных вызовов (если в них нет реальной необходимости): capset, set_tid_address, setgid, setgroups и setuid. Но опять же, почему это проблема? Другими словами, однажды вы можете столкнуться с тем, что после обновления runtime-среды контейнера (вами или, что вероятнее, поставщиком облачных услуг) контейнеры внезапно перестанут запускаться. Однако настоящая сложность в том, что, разрешая процессы, которые вы абсолютно не контролируете, вы привязываете профили к реализации среды исполнения контейнеров.

Это сократит размер профилей seccomp и сделает их менее чувствительными к изменению среды исполнения контейнера. Совет №1: Запускайте контейнеры с AllowPrivilegeEscaltion=false.

2. Задание профилей seccomp на уровне контейнера

Профиль seccomp можно задавать на уровне pod'а:

annotations: seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

… или на уровне контейнера:

annotations: container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"

Обратите внимание, что приведенный выше синтаксис изменится, когда Kubernetes seccomp станет GA (это событие ожидается уже в следующем релизе Kubernetes — 1.18 — прим. перев.).

Среда исполнения частично компенсирует данный недостаток, однако этот контейнер никуда не девается из pod'ов, поскольку используется для настройки их инфраструктуры. Мало кто знает, что в Kubernetes всегда имелся баг, из-за которого профили seccomp применялись к pause-контейнеру.

Проблема же в том, что этот контейнер всегда запускается с AllowPrivilegeEscalation=true, приводя к проблемам, озвученным в пункте 1, и изменить это невозможно.

Так придется делать до тех пор, пока разработчики не исправят баг и новая версия (может быть, 1. Применяя профили seccomp на уровне контейнера, вы избегаете данной ловушки и можете создать профиль, который будет «заточен» под конкретный контейнер. 18?) станет доступна для всех желающих.

Совет №2: Задавайте профили seccomp на уровне контейнера.

В практическом смысле это правило обычно служит универсальным ответом на вопрос: «Почему мой профиль seccomp работает с docker run, но не работает после развертывания в кластере Kubernetes?».

3. Используйте runtime/default только в крайнем случае

В Kubernetes имеется два варианта встроенных профилей: runtime/default и docker/default. Оба реализуются средой исполнения контейнера, а не Kubernetes. Поэтому они могут отличаться в зависимости от используемой среды исполнения и её версии.

Большинство сред исполнения используют реализацию Docker. Другими словами, в результате смены runtime контейнер может получить доступ к другому набору системных вызовов, которые он может использовать или не использовать. Если вы желаете задействовать этот профиль, убедитесь, что он вам подходит.

11, поэтому избегайте его применения. Профиль docker/default считается устаревшим с Kubernetes 1.

Однако если говорить о бизнес-приложениях, работающих в кластерах Kubernetes, я бы взял на себя смелость утверждать, что такой профиль слишком открыт и разработчики должны сконцентрироваться на создании профилей под свои приложения (или типы приложений). По моему мнению, профиль runtime/default прекрасно подходит для тех целей, для которых он создавался: защиты пользователей от рисков, связанных с выполнением команды docker run на их машинах.

Если это невозможно, займитесь профилями под виды приложений, например, создайте расширенный профиль, который включит в себя все веб-API приложения на Golang. Совет №3: Создавайте профили seccomp под конкретные приложения. Только в качестве крайнего средства используйте runtime/default.

Другими словами, у вас не останется оправданий, чтобы не переходить на профили под конкретные приложения. В будущих публикациях я расскажу, как создавать профили seccomp в духе SecDevOps, автоматизировать и тестировать их в пайплайнах.

4. Unconfined — это НЕ вариант

Из первого аудита безопасности Kubernetes выяснилось, что по умолчанию seccomp отключён. Это означает, что если вы не зададите PodSecurityPolicy, которая включит его в кластере, все pod'ы, для которых не определён профиль seccomp, будут работать в режиме seccomp=unconfined.

Подобный подход не рекомендуется специалистами по безопасности. Работа в таком режиме означает, что теряется целый слой изоляции, обеспечивающий защиту кластера.

Совет №4: Ни один контейнер в кластере не должен работать в режиме seccomp=unconfined, особенно в production-средах.

5. «Режим аудита»

Этот момент не уникален для Kubernetes, но всё же попадает в категорию «о чём следует знать ещё до начала».

Дело в том, что у пользователей просто нет возможности проверить их в production-средах, не рискуя «уронить» приложение. Так повелось, что создание профилей seccomp всегда было непростым занятием и в значительной степени основывалось на методе проб и ошибок.

14 появилась возможность запускать части профиля в режиме аудита, записывая в syslog информацию обо всех системных вызовах, но не блокируя их. После появления ядра Linux 4. Активировать этот режим можно с помощью параметра SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp не будет влиять на работу потока, делающего системный вызов, если он не подпадает под какое-либо правило из фильтра, однако информация о системном вызове будет внесена в журнал.

Вот типовая стратегия использования этой возможности:

  1. Разрешить системные вызовы, которые необходимы.
  2. Заблокировать системы вызовы, о которых известно, что они не пригодятся.
  3. Информацию обо всех остальных вызовах записывать в журнал.

Упрощенный пример выглядит следующим образом:

{ "defaultAction": "SCMP_ACT_LOG", "architectures": [ "SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32" ], "syscalls": [ { "names": [ "arch_prctl", "sched_yield", "futex", "write", "mmap", "exit_group", "madvise", "rt_sigprocmask", "getpid", "gettid", "tgkill", "rt_sigaction", "read", "getpgrp" ], "action": "SCMP_ACT_ALLOW" }, { "names": [ "add_key", "keyctl", "ptrace" ], "action": "SCMP_ACT_ERRNO" } ]
}

(medium-mixed-seccomp.json)

Хорошей основой для составления списка является официальная документация Docker. Но помните, что необходимо заблокировать все вызовы, о которых известно, что они не будут использованы, и которые потенциально способны навредить кластеру. В ней подробно объясняется, какие системные вызовы заблокированы в профиле по умолчанию и почему.

Хотя SCMT_ACT_LOG поддерживается ядром Linux с конца 2017 года, в экосистему Kubernetes он вошёл лишь сравнительно недавно. Впрочем, есть один подвох. 14 и runC версии не ниже v1. Поэтому для использования этого метода понадобятся ядро Linux 4. 0-rc9. 0.

Совет №5: Профиль режима аудита для тестирования в production можно создать, комбинируя черный и белый списки, а все исключения записывать в журнал.

6. Используйте белые списки

Формирование белых списков требует дополнительных усилий, поскольку приходится идентифицировать каждый вызов, который может понадобиться приложению, однако этот подход изрядно повышает безопасность:

Чёрный список необходимо будет обновлять всякий раз при добавлении потенциально опасного системного вызова (или опасного флага/опции, если они находятся в чёрном списке). Настоятельно рекомендуется использовать подход на основе белых списков, поскольку он проще и надёжнее. Кроме того, часто можно изменить представление параметра, не меняя его суть и тем самым обойти ограничения чёрного списка.

Для приложений на языке Go я разработал специальный инструмент, который сопровождает приложение и собирает все вызовы, совершенные во время выполнения. Например, для следующего приложения:

package main import "fmt" func main() { fmt.Println("test")
}

… запустим gosystract так:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "\"%s\",\n" .Name}}{{- end}}' application-path

… и получим следующий результат:

"sched_yield", "futex", "write", "mmap", "exit_group", "madvise", "rt_sigprocmask", "getpid", "gettid", "tgkill", "rt_sigaction", "read", "getpgrp", "arch_prctl",

Пока это лишь пример — подробности об инструментарии будут дальше.

Совет №6: Разрешайте только те вызовы, которые вам действительно необходимы, и блокируйте все остальные.

7. Заложите верные основы (или готовьтесь к непредвиденному поведению)

Ядро будет следить за соблюдением профиля независимо от того, что вы в нём прописали. Даже если это не совсем то, чего хотелось. Например, если заблокировать доступ к вызовам вроде exit или exit_group, контейнер не сможет правильно завершить работу и даже простая команда типа echo hi подвесит его на неопределённый срок. В результате вы получите высокую загрузку CPU в кластере:

В таких случаях на выручку может прийти утилита strace — она покажет, в чём может заключаться проблема:


sudo strace -c -p 9331

Убедитесь, что профили содержат все системные вызовы, нужные приложению во время работы.

Совет №7: Внимательно относитесь к мелочам и проверяйте, что все необходимые системные вызовы включены в белый список.

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

P.S. от переводчика

Читайте также в нашем блоге:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть