Seccomp в Kubernetes: 7 речей, про які потрібно знати з самого початку

Прим. перев.: Представляємо до уваги переклад статті старшого інженера з безпеки додатків британської компанії ASOS.com. З нею він розпочинає цикл публікацій, присвячених підвищенню безпеки в Kubernetes завдяки використанню seccomp. Якщо вступ сподобається читачам, ми підемо за автором і продовжимо з його майбутніми матеріалами з цієї теми.

Seccomp в Kubernetes: 7 речей, про які потрібно знати з самого початку

Ця стаття — перша із серії публікацій про те, як створювати профілі 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 у кластері:

Seccomp в Kubernetes: 7 речей, про які потрібно знати з самого початку

У таких випадках на виручку може прийти утиліта strace — вона покаже, у чому може полягати проблема:

Seccomp в Kubernetes: 7 речей, про які потрібно знати з самого початку
sudo strace -c -p 9331

Переконайтеся, що профілі містять усі дзвінки, потрібні програмі під час роботи.

Рада №7: Уважно ставтеся до дрібниць і перевіряйте, що всі необхідні системні дзвінки включені до білого списку.

На цьому перша частина циклу статей про використання seccomp у Kubernetes у дусі SecDevOps добігає кінця. У наступних частинах ми поговоримо про те, чому це важливо та як автоматизувати процес.

PS від перекладача

Читайте також у нашому блозі:

Джерело: habr.com

Додати коментар або відгук