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

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



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

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

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

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

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


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

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "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"
        }
    ]
}

(medium-basic-seccomp.json)

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

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

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

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

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

1. AllowPrivilegeEscalation=false


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

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

Контейнеру, который выполняет банальное 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-среды контейнера (вами или, что вероятнее, поставщиком облачных услуг) контейнеры внезапно перестанут запускаться.

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

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 — прим. перев.).

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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


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

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

После появления ядра Linux 4.14 появилась возможность запускать части профиля в режиме аудита, записывая в syslog информацию обо всех системных вызовах, но не блокируя их. Активировать этот режим можно с помощью параметра 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 он вошёл лишь сравнительно недавно. Поэтому для использования этого метода понадобятся ядро Linux 4.14 и runC версии не ниже v1.0.0-rc9.

Совет №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. от переводчика


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

  • «Безопасность для Docker-контейнеров»;
  • «33+ инструмента для безопасности Kubernetes»;
  • «Docker и Kubernetes в требовательных к безопасности окружениях»;
  • «9 лучших практик по обеспечению безопасности в Kubernetes».
Источник: https://habr.com/ru/company/flant/blog/481114/


Интересные статьи

Интересные статьи

Разберём основы логирования в Docker и Kubernetes, а затем рассмотрим два инструмента, которые можно смело использовать на продакшене: Grafana Loki и стек EFK (Elasticsearch + Fluent ...
Исполнительный директор Intel Боб Свон Корпорация Intel опубликовала финансовый отчёт за II кв. 2020 года, в котором объявила об очередной задержке с переходом на техпроцесс 7 нм...
Введение В своей работе я уже некоторое время использую Flask-Potion — фреймворк, основными достоинствами которого являются: весьма удобная интеграция с SQLAlchemy моделями, автогенерация crud-э...
Несмотря на то, что “в коробке” с Битриксом уже идут модули как для SOAP (модуль “Веб сервисы” в редакции “Бизнес” и старше), так и для REST (модуль “Rest API” во всех редакциях, начиная с...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...