Модуль 4.1: Контексти безпеки
Складність:
[СЕРЕДНЯ]— основна навичка CKSЧас на проходження: 45-50 хвилин
Передумови: знання специфікації Pod з CKA, базові концепції безпеки Linux
Що ви зможете зробити
Розділ «Що ви зможете зробити»Після завершення цього модуля ви зможете:
- Налаштовувати контексти безпеки Pod’а та контейнера, які забезпечують виконання без прав root, відкинуті можливості (capabilities) та файлові системи лише для читання.
- Проводити аудит робочих навантажень на привілейований режим, простори імен хоста, небезпечні можливості та шляхи підвищення привілеїв.
- Діагностувати збої під час запуску та виконання, спричинені
runAsNonRoot, відсутністю точок монтування з правом запису, seccomp або обмеженнями можливостей. - Проєктувати маніфест Pod’а з ешелонованим захистом (defense-in-depth) для Kubernetes 1.35+, який збалансовує принцип найменших привілеїв із потребами застосунку.
Чому цей модуль важливий
Розділ «Чому цей модуль важливий»Гіпотетичний сценарій: ваша команда успадковує простір імен, повний невеликих сервісів, створених під час швидкого запуску продукту. Pod’и запускаються чисто, дашборди розгортань зелені, а власники застосунків наполягають, що нічого поганого немає. Під час аудиту в стилі CKS ви виявляєте, що один вебконтейнер працює як UID 0, інший може писати будь-куди у файловій системі свого образу, а діагностичний Pod досі має privileged: true, тому що комусь під час збою знадобився швидкий доступ до мережевого пристрою. Поки що нічого не зламалося, але радіус ураження (blast radius) будь-якої помилки в застосунку значно більший, ніж потрібно.
Контексти безпеки важливі, тому що Kubernetes не знає автоматично, які мінімальні привілеї потрібні вашому застосунку. Образ контейнера може оголосити користувача за замовчуванням, середовище виконання може додати стандартний набір можливостей, а kubelet може застосувати володіння на рівні Pod’а до змонтованих томів — але ці значення за замовчуванням не те саме, що дизайн безпеки. Контексти безпеки — це місце, де ви перетворюєте розпливчасту інструкцію на кшталт «запусти це робоче навантаження безпечно» на конкретні засоби контролю: який UID має виконувати процес, яка група має володіти змонтованими файлами, чи заблоковано підвищення привілеїв, які можливості Linux залишаються та чи доступна коренева файлова система для запису.
Для іспиту CKS і для роботи на проді важлива навичка — це не запам’ятовування одного посиленого YAML-шаблону. Важлива навичка — це читати специфікацію Pod’а, передбачати, як середовище виконання поєднає налаштування рівня Pod’а та рівня контейнера, а потім обирати найменш інвазивний засіб контролю, який усе ще дозволяє застосунку працювати. У цьому модулі ви напрацюєте таке судження, посилюючи захист Pod’а шарами, налагоджуючи збої, які ці шари можуть спричинити, та пов’язуючи кожне поле маніфесту з тією поведінкою Linux, яку воно змінює.
Що контролюють контексти безпеки
Розділ «Що контролюють контексти безпеки»Контекст безпеки — це спосіб Kubernetes передати інструкції щодо привілеїв та контролю доступу зі специфікації Pod’а до середовища виконання контейнера і, зрештою, до ядра Linux. Уявіть це як контракт між описом робочого навантаження від планувальника та моделлю процесів вузла. API-об’єкт каже, які межі ідентичності, файлової системи, можливостей та системних викликів очікує робоче навантаження; kubelet та середовище виконання перетворюють ці поля на атрибути процесу до того, як стартує перший процес контейнера.
Перша практична відмінність — це область дії. spec.securityContext застосовує значення за замовчуванням до всього Pod’а, тоді як containers[].securityContext застосовується до одного контейнера і може перевизначити поля, що існують на обох рівнях. Цей поділ корисний, бо деякі налаштування природно описують відношення в межах усього Pod’а — як-от спільне володіння томом, — тоді як інші налаштування належать одному процесу, наприклад чи потрібна сайдкару конкретна можливість. Завдання CKS часто ховають помилку саме в цій поведінці перевизначення.
┌─────────────────────────────────────────────────────────────┐│ SECURITY CONTEXT SCOPE │├─────────────────────────────────────────────────────────────┤│ ││ Pod-Level (spec.securityContext): ││ ├── runAsUser - UID for all containers ││ ├── runAsGroup - GID for all containers ││ ├── fsGroup - GID for volume ownership ││ ├── runAsNonRoot - Prevent running as root ││ ├── supplementalGroups - Additional group memberships ││ ├── seccompProfile - Seccomp profile ││ └── sysctls - Kernel parameters ││ ││ Container-Level (containers[].securityContext): ││ ├── runAsUser - Override pod-level UID ││ ├── runAsGroup - Override pod-level GID ││ ├── runAsNonRoot - Container-specific check ││ ├── privileged - Full host access (dangerous!) ││ ├── allowPrivilegeEscalation - Prevent privilege gain ││ ├── capabilities - Linux capabilities ││ ├── readOnlyRootFilesystem - Immutable container ││ ├── seccompProfile - Container-specific seccomp ││ └── seLinuxOptions - SELinux labels ││ ││ Container settings OVERRIDE pod settings ││ │└─────────────────────────────────────────────────────────────┘Діаграму навмисно поділено на поля рівня Pod’а та рівня контейнера, бо саме так слід читати кожне робоче навантаження. Почніть зі значень Pod’а за замовчуванням, потім перевірте кожен контейнер на перевизначення, а тоді перевірте поля Pod’а поза securityContext, які все одно впливають на ізоляцію. Специфікація може виглядати посиленою згори, тоді як один контейнер тихо повертається до небезпечної ідентичності чи простору імен. Підсумкова фактична поведінка — це комбінація, а не найгарніший блок YAML. Налаштування профілів SELinux та AppArmor (seLinuxOptions, анотації) виходять за межі цього модуля; тут зосередьтеся на полях UID/GID, можливостей, файлової системи та seccomp.
Контексти безпеки не замінюють політику допуску. Якщо користувач може створювати Pod’и в просторі імен, цей користувач також може записати небезпечний контекст безпеки, доки щось не перевірить запит перед його збереженням. Pod Security Admission, розглянутий у наступному модулі, — це захисний механізм на боці кластера, який відхиляє Pod’и, що порушують базову (baseline) або обмежену (restricted) політику. Цей модуль зосереджений на навичці авторства робочих навантажень: написанні, аудиті та налагодженні полів, які політика допуску згодом забезпечуватиме.
Наступний приклад показує поширену відправну точку без прав root. runAsNonRoot: true сам по собі не є призначенням UID. Це інструкція валідації, яка каже Kubernetes, що контейнер не повинен запускатися як UID 0. Якщо метадані образу або явний runAsUser показують root, kubelet відхиляє запуск, а не мовчки обирає безпечніший обліковий запис.
apiVersion: v1kind: Podmetadata: name: non-root-podspec: securityContext: runAsNonRoot: true # Pod-level enforcement containers: - name: app image: nginx securityContext: runAsUser: 1000 # Must specify non-root UID runAsGroup: 1000 volumeMounts: - name: cache mountPath: /var/cache/nginx - name: run mountPath: /var/run - name: tmp mountPath: /tmp volumes: - name: cache emptyDir: {} - name: run emptyDir: {} - name: tmp emptyDir: {}
# If image tries to run as root (UID 0), pod fails to start:# Error: container has runAsNonRoot and image will run as root# Stock nginx as UID 1000 also needs writable cache/runtime paths (emptyDir above).Зупиніться та передбачте: якщо ви приберете runAsUser: 1000 з цього маніфесту, але залишите runAsNonRoot: true, що має статися, коли обраний образ оголошує або передбачає UID 0? Корисна ментальна модель полягає в тому, що runAsNonRoot — це сторож, а не інструмент полагодження. Він блокує запуск процесу root, але не редагує образ, не створює користувача і не виправляє права доступу до файлів усередині образу.
Kubernetes також надає налаштування, які не входять до securityContext, але мають перевірятися з тією самою серйозністю. hostPID, hostNetwork та hostIPC приєднують простори імен хоста замість використання ізольованих просторів імен для Pod’а. Pod із видимістю процесів хоста, монтуванням хоста з правом запису та неправильним набором можливостей може стати проблемою рівня вузла навіть тоді, коли контекст безпеки на рівні контейнера виглядає скромно. Розглядайте ці поля як частину того самого проходу перевірки.
apiVersion: v1kind: Podmetadata: name: host-namespacesspec: hostPID: true # Can see all processes on the node hostNetwork: true # Uses node's network namespace hostIPC: true # Can access node's IPC mechanisms containers: - name: app image: nginxНалаштування просторів імен хоста не завжди зловмисні. Деякі агенти вузла, компоненти CNI, драйвери CSI та інструменти спостережуваності законно потребують ширшого огляду вузла, ніж звичайний Pod застосунку. Питання аудиту — чи виправдовує призначення робочого навантаження такий огляд і чи поєднано доступ до простору імен із жорсткими можливостями, монтуваннями хоста лише для читання, де це можливо, та засобами контролю допуску, які не дають звичайним просторам імен застосунків копіювати цей виняток.
Межі ідентичності та файлової системи
Розділ «Межі ідентичності та файлової системи»Найпомітніша частина контексту безпеки — це ідентичність процесу. Linux використовує числові ідентифікатори користувача та групи для перевірок володіння файлами та прав доступу, і Kubernetes передає налаштовані UID та GID у процес контейнера. Імена на кшталт nginx чи appuser — це зручності з /etc/passwd; ядро забезпечує числа. Саме тому контексти безпеки використовують runAsUser, runAsGroup, fsGroup та додаткові групи замість того, щоб покладатися на дружнє ім’я користувача в образі.
Запуск без прав root цінний, тому що багато операцій файлової системи та ядра заборонені для ненульових UID, доки конкретна можливість не поверне їх назад. Це не магічна пісочниця, і вона не робить помилки застосунку нешкідливими, але вона усуває велику категорію легких подальших дій після компрометації. Зловмисник, який може виконати код як UID без прав root, зазвичай мусить знайти додаткову неправильну конфігурацію, перш ніж змінювати системні файли, прив’язуватися до привілейованих портів чи привласнювати володіння змонтованими даними.
Компроміс полягає в тому, що образи, побудовані на припущеннях про root, можуть давати збій. Вони можуть писати PID-файли під /var/run, створювати файли кешу під /var/cache, змінювати володіння під час запуску чи прив’язуватися до низького порту. Хороше виправлення змінює робоче навантаження так, щоб воно відповідало найменшим привілеям; слабке виправлення прибирає сторожа. На практиці це означає вибір образу без прав root, перезбирання образу з правильним володінням, зміну порту застосунку чи монтування конкретних шляхів із правом запису.
Наступний приклад allowPrivilegeEscalation додає другий шар. Це поле контролює, чи може процес отримати більше привілеїв, ніж його батьківський процес, що відповідає поведінці Linux no_new_privs у середовищі виконання. Це особливо важливо, коли образ містить бінарні файли setuid або setgid. Без цього налаштування процес без прав root усе ще може виконати бінарний файл, який тимчасово надає підвищені права.
apiVersion: v1kind: Podmetadata: name: no-escalationspec: containers: - name: app image: nginx securityContext: allowPrivilegeEscalation: false # Prevent setuid, setgid
# This prevents:# - setuid binaries from gaining privileges# - Container processes from becoming root# - Exploits that rely on privilege escalationПеред запуском передбачте: який вивід ви очікуєте, якщо образ контейнера має скрипт запуску, що намагається змінити володіння під каталогом, який належить root? Межа ідентичності та межа підвищення привілеїв працюють разом, але вони не надають права запису до шляхів, які користувач не може змінити. Запуск усе одно зазнає невдачі, якщо образу потрібні каталоги, що належать root, а ви не перезібрали образ і не надали том із правом запису за конкретним шляхом.
readOnlyRootFilesystem обробляє іншу частину історії після компрометації. Шари образу контейнера стають доступними лише для читання, тож процес не може скинути модифікований бінарний файл, перезаписати конфігурацію в образі чи зберегти скрипт за довільним шляхом. Це налаштування не робить кожен запис неможливим. Воно проштовхує легітимні записи в оголошені томи, що полегшує перевірку, бо поверхня запису видима в специфікації Pod’а.
apiVersion: v1kind: Podmetadata: name: readonly-podspec: containers: - name: app image: nginx securityContext: readOnlyRootFilesystem: true # Can't write to container filesystem volumeMounts: - name: tmp mountPath: /tmp # Must provide writable volume for temp files - name: cache mountPath: /var/cache/nginx - name: run mountPath: /var/run volumes: - name: tmp emptyDir: {} - name: cache emptyDir: {} - name: run emptyDir: {}Правило проєктування — монтувати томи з правом запису для даних, а не для лінощів. /tmp, каталоги кешу застосунку, каталоги завантажень та сокети середовища виконання — це розумні кандидати, коли застосунок цього вимагає. Якщо зробити /etc, /usr чи весь каталог застосунку доступним для запису, це часто відтворює той ризик, який ви намагалися усунути. Якщо застосунок мусить генерувати конфігурацію, надайте перевагу init-контейнеру, який пише у спільний том, а потім монтуйте цей том лише для читання в основний контейнер, коли це можливо.
fsGroup вирішує одну поширену проблему з томами. Коли Pod монтує том, kubelet може влаштувати володіння групою так, щоб процес без прав root міг писати в том, не запускаючись як root. Це робить fsGroup справою рівня Pod’а, бо томи спільні в межах Pod’а. Ціна полягає в тому, що рекурсивні зміни володіння можуть бути дорогими на великих томах, тож сучасні кластери також використовують fsGroupChangePolicy, коли їм потрібно уникнути повторних глибоких обходів прав доступу.
Перевизначення на рівні контейнера корисні, але ризиковані. Наступний маніфест показує значення Pod’а за замовчуванням UID 1000 та перевизначення сайдкара на UID 2000. Це легітимний патерн, коли контейнери використовують різні образи з різним володінням файлами, але це також місце, де випадковий runAsUser: 0 може скасувати стандарт рівня Pod’а. Під час перевірки читайте перевизначення як навмисні винятки, що потребують причини.
apiVersion: v1kind: Podmetadata: name: mixed-contextspec: securityContext: runAsUser: 1000 # Default for all containers runAsGroup: 1000 containers: - name: app image: myapp # Inherits runAsUser: 1000 from pod - name: sidecar image: sidecar securityContext: runAsUser: 2000 # Overrides pod-level setting # This container runs as UID 2000, not 1000Під час налагодження збоїв ідентичності опирайтеся спокусі залатати всі поля одразу. Спершу визначте, чи стався збій на етапі допуску, на етапі запуску чи під час виконання. Збої допуску зазвичай походять від політики. Збої запуску часто згадують runAsNonRoot або користувача образу. Збої під час виконання зазвичай кажуть permission denied, read-only file system чи operation not permitted. Повідомлення підказує, яку межу ви перетнули.
Можливості, підвищення привілеїв та ризик хоста
Розділ «Можливості, підвищення привілеїв та ризик хоста»Можливості Linux (capabilities) розщеплюють історичну могутність root на менші іменовані дозволи. Процес може бути UID 0, але все одно не мати певної можливості, а процес без прав root може отримати вузько окреслену можливість, коли робоче навантаження її справді потребує. Контейнери покладаються на цей поділ, бо багато застосунків потребують однієї привілейованої дії — як-от прив’язки до низького порту, — не потребуючи кожного дозволу, який традиційно мав root.
Важлива деталь у тому, що набір можливостей за замовчуванням усе одно ширший, ніж потрібно більшості контейнерів застосунків. Простому HTTP-сервісу на порту 8080 зазвичай не потрібне формування пакетів, зміни володіння, сирі сокети чи переходи setuid. Залишати можливості за замовчуванням, бо «Pod працює нормально», — це як лишати кожен ключ на зв’язці, бо одні двері все ще треба відчиняти. Безпечніший робочий процес — відкинути всі можливості, а потім додати назад ту, яку ви можете обґрунтувати.
┌─────────────────────────────────────────────────────────────┐│ LINUX CAPABILITIES │├─────────────────────────────────────────────────────────────┤│ ││ Capabilities split root powers into fine-grained units: ││ ││ CAP_NET_BIND_SERVICE - Bind to ports < 1024 ││ CAP_NET_ADMIN - Configure network interfaces ││ CAP_NET_RAW - Use raw sockets (ping, etc.) ││ CAP_SYS_ADMIN - Many syscalls (mount, etc.) ││ CAP_SYS_PTRACE - Debug other processes ││ CAP_CHOWN - Change file ownership ││ CAP_DAC_OVERRIDE - Bypass file permissions ││ CAP_SETUID/SETGID - Change UID/GID ││ CAP_KILL - Send signals to any process ││ ││ Default container capabilities (Docker): ││ CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, ││ SETGID, SETUID, SETPCAP, NET_BIND_SERVICE, NET_RAW, ││ SYS_CHROOT, MKNOD, AUDIT_WRITE, SETFCAP ││ ││ Best practice: Drop ALL, add only what's needed ││ │└─────────────────────────────────────────────────────────────┘Імена можливостей у Kubernetes опускають префікс CAP_. Наприклад, Linux називає дозвіл на низький порт CAP_NET_BIND_SERVICE, тоді як маніфести Kubernetes використовують NET_BIND_SERVICE. Ця відмінність у найменуванні дрібна, але важлива для іспиту, бо неправильно написана можливість — це не те саме, що відмовлена можливість. Завжди читайте поле API як список можливостей Linux із прибраним префіксом.
apiVersion: v1kind: Podmetadata: name: minimal-capsspec: containers: - name: app image: nginx securityContext: capabilities: drop: - ALL # Drop all capabilities add: - NET_BIND_SERVICE # Only add what's neededЗупиніться та передбачте: ви встановлюєте capabilities.drop: ["ALL"] на контейнері, який прив’язується до порту 80, і процес завершується з permission denied. Діагностуйте режим збою, перш ніж додавати можливості. Якщо логи показують mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied), процес ніколи не дійшов до прив’язки порту — спершу полагодьте шляхи кешу/середовища виконання з правом запису через монтування emptyDir, fsGroup чи володіння образу. Якщо nginx запускається чисто, але не може прив’язати порт 80, збій прив’язки окремий: процесам без прав root потрібен NET_BIND_SERVICE (або високий порт, як-от 8080). Найменша можливість, яку треба додати назад для прив’язки до низького порту, — це NET_BIND_SERVICE, але кращим довгостроковим питанням є те, чи слід застосунку слухати на 8080. Зміна порту застосунку повністю усуває потребу в можливості, тоді як додавання можливості зберігає вузький виняток, який ви маєте переглянути згодом.
allowPrivilegeEscalation: false має йти разом зі зменшенням можливостей. Відкидання можливостей прибирає дозволи з процесу, тоді як блокування підвищення привілеїв запобігає поширеним шляхам їх повернення через бінарні файли setuid чи пов’язані механізми. Документація Kubernetes зазначає, що підвищення привілеїв завжди true для привілейованих контейнерів і для контейнерів із CAP_SYS_ADMIN, тож allowPrivilegeEscalation: false не є універсальним перевизначенням. Якщо ви надаєте широку могутність, середовище виконання не може вдавати, що процес жорстко обмежений.
privileged: true — це поле, яке має змусити вас зупинитися. Привілейований контейнер отримує доступ, набагато ближчий до моделі могутності хоста, включно з широким доступом до пристроїв та набором можливостей, що підриває звичайну межу контейнера. Існують легітимні випадки використання системного рівня, але звичайні Pod’и застосунків не повинні використовувати це поле як обхідний шлях для відсутнього дозволу. Якщо робоче навантаження не є агентом вузла, драйвером сховища, мережевим компонентом чи жорстко контрольованим інструментом обслуговування, відповідь за замовчуванням — ні.
# DON'T DO THIS in productionapiVersion: v1kind: Podmetadata: name: privileged-podspec: containers: - name: app image: nginx securityContext: privileged: true # Full access to host!
# privileged: true means:# - Access to all host devices# - Can load kernel modules# - Can modify iptables# - Dramatically increases node-compromise blast radius; may enable# container breakout depending on kernel/runtime bugs and host config# - ONLY use for system-level daemons (CNI, CSI drivers)Гіпотетичний сценарій: команда застосунку не може налагодити захоплення пакетів під час інциденту, тож вона змінює вебдеплоймент на privileged: true і планує повернути назад після наради. Наступний спринт постачає інші зміни, специфікація Pod’а лишається привілейованою, а пізніша помилка застосунку дає зловмиснику шелл із набагато більшим впливом на вузол, ніж сервіс будь-коли потребував. Безпечніший патерн реагування на інциденти — використовувати виділений процес налагодження з явними часовими обмеженнями, окремим RBAC та переглянутим винятком, а не розширювати виробничий деплоймент.
Поля просторів імен хоста посилюють привілейований ризик, бо вони прибирають ізоляцію навколо ідентифікаторів процесів, мережі чи IPC. hostPID: true дозволяє Pod’у бачити процеси вузла. hostNetwork: true розміщує Pod безпосередньо в мережевому просторі імен вузла, що може вплинути на конфлікти портів та припущення мережевої політики. hostIPC: true відкриває механізми міжпроцесної взаємодії, які звичайним Pod’ам застосунків рідко потрібні. Ці поля не приховані, тож дисциплінована перевірка може швидко їх виловити.
Комбінація важить більше, ніж будь-яке окреме поле. Pod із hostPID: true, монтуванням hostPath та SYS_PTRACE дуже відрізняється від Pod’а, який лише встановлює runAsUser: 1000. Pod із NET_ADMIN та hostNetwork: true може впливати на мережу так, як звичайний вебсервіс не може. Проводьте аудит полів контексту безпеки та полів просторів імен хоста разом, бо зловмисники зчіплюють дозволи так само, як оператори компонують можливості.
Seccomp та повний посилений Pod
Розділ «Seccomp та повний посилений Pod»Seccomp фільтрує системні виклики між процесом та ядром. Це важливо, тому що контейнери спільно використовують ядро хоста, навіть коли простори імен та cgroups ізолюють багато ресурсів. Профіль seccomp за замовчуванням середовища виконання блокує чи обмежує сімейства системних викликів, які звичайним застосункам не потрібні. Це не заміна виконання без прав root чи відкидання можливостей, але воно зачиняє інші двері, зменшуючи інтерфейс ядра, доступний процесу.
У Kubernetes 1.35+ практична базова лінія — це seccompProfile.type: RuntimeDefault, доки робоче навантаження не має виміряної причини використовувати локальний профіль. Unconfined слід розглядати як виняток, бо він прибирає фільтр на рівні ядра. Власний профіль Localhost може бути потужним, але він також створює накладні витрати на керування вузлами, бо файл профілю має існувати там, де kubelet його очікує. Для більшості завдань CKS безпечна відповідь — RuntimeDefault.
Повний посилений Pod нижче поєднує ідентичність, файлову систему, можливості, seccomp та засоби контролю ресурсів. Обмеження ресурсів — це не контексти безпеки, але вони належать до тієї самої розмови про посилення, бо неконтрольовані пам’ять та CPU можуть стати шляхами відмови в обслуговуванні. Приклад навмисно використовує образ-заповнювач застосунку, щоб ви зосередилися на формі специфікації, а не на поведінці запуску одного вендорського образу.
apiVersion: v1kind: Podmetadata: name: hardened-podspec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault # Block dangerous kernel syscalls containers: - name: app image: myapp:1.0 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL resources: # Prevent resource exhaustion (DoS) limits: memory: "128Mi" cpu: "500m" requests: memory: "64Mi" cpu: "250m" volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}Читайте маніфест згори донизу і питайте, що дає кожен шар. Ідентичність рівня Pod’а встановлює користувача та групу за замовчуванням. fsGroup робить змонтовані дані доступними для цієї групи. RuntimeDefault зменшує поверхню системних викликів для кожного контейнера, що його успадковує. Блок рівня контейнера потім запобігає підвищенню привілеїв, прибирає записи в шари образу та відкидає можливості. Жодне з цих полів окремо не є повною відповіддю; разом вони зменшують кілька незалежних шляхів збою.
Цей шаровий підхід також пояснює, чому посилені Pod’и можуть давати збій так, як незахищені Pod’и не дають. Якщо образ очікує запису під /var/cache, коренева файлова система лише для читання виявить це припущення. Якщо процес очікує прив’язки до порту 80 як UID без прав root, відкидання можливостей виявить це припущення. Якщо точка входу образу намагається змінити володіння під час запуску, ідентичність без прав root виявить це припущення. Збій після посилення часто є корисною інформацією про контракт образу.
Правильне виправлення залежить від того, яке припущення дало збій. Перезбирання образу з правильним володінням зазвичай краще, ніж надання CHOWN. Слухання на високому порту зазвичай краще, ніж додавання NET_BIND_SERVICE. Монтування emptyDir на /tmp зазвичай краще, ніж вимкнення readOnlyRootFilesystem. Ці вибори тримають контекст безпеки вузьким і роблять потрібні винятки видимими для рецензентів.
Контексти безпеки також мають бути читабельними для наступної людини. Розміщуйте значення для всього Pod’а за замовчуванням на рівні Pod’а, розміщуйте винятки для конкретного контейнера на рівні контейнера і уникайте дублювання полів, доки дублювання не прояснює навмисне перевизначення. Коли кожен контейнер повторює той самий довгий блок, рецензентам важче знайти той один контейнер, який відрізняється. Коли значення рівня Pod’а за замовчуванням ясне, а винятки локальні, маніфест розповідає власну історію.
Діагностика в стилі іспиту на практиці
Розділ «Діагностика в стилі іспиту на практиці»Завдання CKS часто дають вам Pod, що дає збій, і просять найменше безпечне виправлення. Хитрість — діагностувати клас збою перед редагуванням YAML. Помилка запуску про runAsNonRoot вказує на користувача образу чи вибір UID. Помилка запису під час виконання вказує на readOnlyRootFilesystem чи шляхи томів. Помилка operation not permitted часто вказує на відсутню можливість чи обмеження seccomp. Найшвидший шлях — зіставити симптом із межею.
Перший сценарій виправляє Pod, який інакше працював би як root. Незахищена версія опускає контекст безпеки і рівня Pod’а, і рівня контейнера. Безпечна версія зберігає сторожа без прав root і встановлює числовий UID, щоб середовище виконання мало однозначну ідентичність. Вона також блокує підвищення привілеїв на рівні контейнера, бо це налаштування стосується процесу, а не спільного володіння томом Pod’а.
# Before (insecure)apiVersion: v1kind: Podmetadata: name: insecure-podspec: containers: - name: app image: nginx
# After (secure) — exam-minimum identity fields onlyapiVersion: v1kind: Podmetadata: name: secure-podspec: securityContext: runAsNonRoot: true runAsUser: 1000 containers: - name: app image: nginx securityContext: allowPrivilegeEscalation: falseМаніфест вище показує найменше виправлення ідентичності в стилі іспиту. Воно лише ілюстративне для стокового nginx: без шляхів кешу/середовища виконання з правом запису UID 1000 дає збій на mkdir() під /var/cache/nginx ще до того, як постане будь-яке питання можливостей. Робочий посилений лабораторний маніфест далі в цьому модулі додає потрібні монтування emptyDir. На іспиті завдання може вимагати лише зміни контексту безпеки. Під час виробничої перевірки ви також перевірили б, чи може UID 1000 прочитати налаштований контент, чи потрібні nginx каталоги кешу або середовища виконання з правом запису та чи може сервіс використовувати високий порт, щоб не була потрібна можливість низького порту.
Другий сценарій обробляє робоче навантаження, якому потрібно прив’язатися до порту 80 без запуску як root. Це класичний виняток можливості. Маніфест спершу відкидає кожну можливість, а потім додає NET_BIND_SERVICE назад. Він також зберігає allowPrivilegeEscalation: false, бо додавання однієї можливості не повинно перетворитися на загальний шлях привілеїв. Виняток видимий, вузький і прив’язаний до конкретної вимоги застосунку. Стоковому nginx як UID 1000 потрібні шляхи середовища виконання з правом запису ще до того, як можна навіть дійти до прив’язки порту — монтування emptyDir нижче задовольняють каталоги кешу та PID/сокетів, які nginx створює під час запуску.
# Pod needs to bind to port 80 but shouldn't run as rootcat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: web-serverspec: securityContext: runAsNonRoot: true runAsUser: 1000 containers: - name: nginx image: nginx securityContext: capabilities: drop: - ALL add: - NET_BIND_SERVICE # Allow binding to port 80 allowPrivilegeEscalation: false volumeMounts: - name: cache mountPath: /var/cache/nginx - name: run mountPath: /var/run - name: tmp mountPath: /tmp volumes: - name: cache emptyDir: {} - name: run emptyDir: {} - name: tmp emptyDir: {}EOF
kubectl wait --for=condition=Ready pod/web-server --timeout=60skubectl exec web-server -- sh -c 'grep -q ":0050" /proc/net/tcp && echo "nginx listening on port 80"'Який підхід ви обрали б тут і чому: залишити порт 80 з NET_BIND_SERVICE чи змінити застосунок, щоб він слухав на 8080, і дозволити Сервісу відкрити порт 80? Другий варіант зазвичай перемагає, коли ви володієте застосунком, бо він усуває виняток можливості. Перший варіант може бути прийнятним, коли застарілий бінарний файл не може змінити свій порт прослуховування, а виняток задокументовано.
Третій сценарій додає кореневу файлову систему лише для читання, зберігаючи при цьому шляхи, які nginx зазвичай потребує для файлів кешу та середовища виконання. Це безпечний патерн: почати з лише для читання, потім зробити мінімальні шляхи запису явними. Якщо застосунку також потрібен /tmp, додайте том туди. Якщо йому потрібні логи, спершу надайте перевагу stdout та stderr, а потім додайте каталог логів із правом запису лише тоді, коли інтеграція на основі файлів справді цього вимагає.
# Add read-only filesystem with required writable mountscat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: readonly-nginxspec: containers: - name: nginx image: nginx securityContext: readOnlyRootFilesystem: true volumeMounts: - name: cache mountPath: /var/cache/nginx - name: run mountPath: /var/run volumes: - name: cache emptyDir: {} - name: run emptyDir: {}EOFНайшвидші команди налагодження — це ті, що показують фактичну специфікацію та події. kubectl get -o yaml показує, що було збережено. JSONPath допомагає швидко переглянути одне поле. kubectl describe показує збої запуску, повідомлення допуску та текст подій. Ви не шукаєте кожної можливої деталі; ви шукаєте першу межу, яка пояснює спостережуваний збій. Замініть mypod на ім’я вашого Pod’а у фрагментах нижче.
# Check pod's effective security contextkubectl get pod mypod -o yaml | grep -A 20 securityContext
# Check container's security contextkubectl get pod mypod -o jsonpath='{.spec.containers[0].securityContext}' | jq .
# Check if pod failed due to security contextkubectl describe pod mypod | grep -i error
# Common errors:# "container has runAsNonRoot and image will run as root"# "unable to write to read-only filesystem"# "operation not permitted" (missing capability)Не сприймайте ці команди як заміну розуміння специфікації. Це спосіб підтвердити гіпотезу, яку ви вже сформували із симптому. Якщо подія каже runAsNonRoot, перевірте користувача образу та runAsUser. Якщо логи процесу кажуть файлова система лише для читання, перевірте монтування томів. Якщо системний виклик чи привілейована операція дає збій, перевірте можливості, seccomp та чи слід операцію взагалі дозволяти.
Аудит наявного робочого навантаження
Розділ «Аудит наявного робочого навантаження»Аудит Pod’а на ризик контексту безпеки відрізняється від написання чистого маніфесту з нуля. У переписуванні ви можете обрати охайну базову лінію і додати лише потрібні вам винятки. В аудиті робоче навантаження вже існує, образ може мати незадокументовані припущення, а небезпечне поле може бути поза очевидним блоком securityContext. Ваше завдання — реконструювати фактичну модель привілеїв перед тим, як пропонувати латку.
Почніть із Pod’а в тому вигляді, як його зберігає Kubernetes, а не з шаблону, який хтось вставив у тікет. Контролери, чарти Helm, накладки Kustomize, мутаційні вебхуки допуску та встановлення значень за замовчуванням можуть змінити фінальну специфікацію Pod’а. Прочитайте securityContext рівня Pod’а, кожен securityContext рівня контейнера, init-контейнери та ефемерні контейнери, якщо вони існують. Потім прочитайте hostPID, hostNetwork, hostIPC, томи hostPath, сервісний акаунт та монтування томів, бо ці поля можуть зробити скромний на вигляд контейнер набагато потужнішим.
Аудит ідентичності ставить одне конкретне питання: який числовий UID та GID використовуватиме кожен процес під час запуску? Якщо контейнер не має явного runAsUser, вам треба знати, чи оголошує образ користувача без прав root. Якщо образ за замовчуванням має root, runAsNonRoot: true виявить проблему під час запуску. Якщо образ за замовчуванням має іменованого користувача, пам’ятайте, що Kubernetes та Linux зрештою дбають про числовий UID за цим іменем.
Аудит файлової системи ставить питання, куди процес може писати після запуску. readOnlyRootFilesystem: true має значення лише тоді, коли ви також перевіряєте монтування томів із правом запису. Pod, який робить /tmp доступним для запису, — це нормально. Pod, який монтує широкий hostPath чи робить каталог бінарних файлів застосунку доступним для запису, заслуговує на пильнішу перевірку. Аудит має розрізняти дані часу виконання від конфігурації та виконуваного контенту, бо ці категорії мають дуже різні профілі ризику.
Аудит можливостей має починатися зі списку add, а потім підтверджувати список drop. Маніфест, який додає SYS_ADMIN, NET_ADMIN чи SYS_PTRACE, потребує сильнішого пояснення, ніж маніфест, який додає NET_BIND_SERVICE. Маніфест, який відкидає ALL і додає одну можливість, легше перевірити, ніж маніфест, який зберігає значення за замовчуванням і прибирає одну ризиковану можливість. Привілейований режим замикає накоротко це міркування, бо він надає широку могутність замість вузького винятку можливості.
Аудит просторів імен шукає комбінації. hostNetwork: true із NET_ADMIN — це дуже інший ризик, ніж hostNetwork: true на жорстко контрольованому експортері вузла, який лише читає метрики. hostPID: true стає серйознішим у поєднанні з інструментами інспекції процесів, SYS_PTRACE чи монтуваннями хоста. Не оцінюйте ці поля ізольовано. Оцінюйте шлях, який вони створюють від компрометації застосунку до впливу на вузол.
Зупиніться та передбачте: Pod має runAsNonRoot: true, відкидає всі можливості та використовує RuntimeDefault, але він також монтує /var/lib/kubelet із хоста з правом читання-запису. Чи назвали б ви це робоче навантаження посиленим? Відповідь має бути ні, бо потужне монтування хоста може переважити інакше сильні засоби контролю на рівні процесу. Перевірка контексту безпеки має включати дані та межі простору імен навколо процесу.
Мітки допуску — це частина аудиту, навіть якщо вони не є полями робочого навантаження. Якщо простір імен забезпечує обмежений (restricted) профіль, багато небезпечних налаштувань буде відхилено ще до того, як Pod існуватиме. Якщо він лише попереджає чи проводить аудит, небезпечні Pod’и все одно можуть працювати, виробляючи сигнали попередження чи аудиту. Ця відмінність змінює ваш план виправлення. У просторі імен у режимі забезпечення виправте маніфест перед розгортанням; у просторі імен у режимі попередження знайдіть уже запущені робочі навантаження, що потребують прибирання.
Багатоконтейнерні Pod’и заслуговують на додаткову увагу, бо спільні ресурси розмивають володіння. Один контейнер може працювати як безпечний UID, тоді як інший сайдкар має перевизначення на root. Один контейнер може мати кореневу файлову систему лише для читання, тоді як init-контейнер пише згенеровані файли у спільний том. Ці патерни можуть бути легітимними, але аудит має пояснити, який контейнер потребує кожного винятку і чи дозволяє спільний том слабшому контейнеру вплинути на сильніший.
Докази часу виконання мають підтвердити статичне читання. Події кажуть вам, чому Pod дав збій. Перевірки процесів можуть підтвердити фактичний UID, коли контейнер запущено. Тести запису можуть довести, чи справді коренева файлова система доступна лише для читання. Операції, чутливі до можливостей, можуть підтвердити, чи потрібен виняток. Використовуйте докази часу виконання для валідації вашої інтерпретації, але не використовуйте успішний тест як доказ, що права доступу безпечні.
Для виправлення в стилі CKS створіть латку, яка спершу прибирає поле найвищого ризику, а потім відновлює функцію застосунку з вузькою альтернативою. Приберіть privileged: true перед налаштуванням менших полів. Замініть виконання як root на числовий UID без прав root перед поліруванням володіння томами. Відкиньте всі можливості перед додаванням конкретного винятку. Цей порядок важливий, бо він швидко зменшує найбільший радіус ураження, тримаючи поведінку робочого навантаження придатною для тестування.
Результат аудиту має бути коротким технічним аргументом, а не просто YAML-діфом. Назвіть ризиковане поле, конкретний ризик, який воно створює, вимогу застосунку, якщо вона є, та безпечнішу заміну. Це пояснення — те, що дозволяє іншому інженеру вирішити, чи належить виняток у системний простір імен, простір імен застосунку чи нікуди взагалі. Хороша робота з контекстом безпеки видима в маніфесті та захищувана під час перевірки.
Патерни та антипатерни
Розділ «Патерни та антипатерни»Найсильніший патерн — спроєктувати робоче навантаження навколо найменших привілеїв перед першим виробничим розгортанням. Це означає вибір образу, який може працювати як UID без прав root, розміщення стану з правом запису під передбачуваними каталогами, використання stdout для логів та уникнення привілейованих портів, коли застосунку вони не потрібні. Контексти безпеки легше тримати суворими, коли контракт образу чистий. Доопрацювання їх після місяців припущень на основі root часто перетворюється на довгий список винятків.
| Патерн | Коли його використовувати | Чому він працює | Міркування щодо масштабування |
|---|---|---|---|
| Значення рівня Pod’а за замовчуванням із локальними винятками | Кілька контейнерів спільно використовують той самий UID, GID, профіль seccomp чи групу томів | Рецензенти бачать заплановану базову лінію один раз і зосереджуються на контейнерах, що відрізняються | Тримайте винятки біля контейнера, який їх потребує, і вимагайте коментаря чи винятку політики для ризикованих полів |
| Спершу відкинути всі можливості | Контейнери застосунків, яким не потрібні root-можливості Linux | Маніфест починається з нульових ambient-можливостей і додає лише обґрунтований дозвіл | Побудуйте невеликий каталог схвалених винятків можливостей, щоб перевірки лишалися узгодженими |
| Файлова система образу лише для читання плюс явні томи з правом запису | Сервіси, які пишуть кеш, сокети, тимчасові файли чи дані завантажень | Легітимні записи видимі у volumeMounts, а шари образу лишаються незмінними під час виконання | Стандартизуйте шляхи запису в базових образах, щоб команди не монтували широкі каталоги |
| Seccomp RuntimeDefault | Загальні Pod’и застосунків Linux на Kubernetes 1.35+ | Середовище виконання блокує ризиковані патерни системних викликів без потреби у власному профілі | Відстежуйте винятки, бо власні профілі створюють роботу з розповсюдження та життєвого циклу на вузлах |
Антипатерни зазвичай починаються як зручність. Розробника заблоковано, інцидент гучний, або образ було зібрано за роки до того, як кластер прийняв сильніші стандарти. Виправлення, що змушує Pod швидко запускатися, може стати постійним, якщо ніхто не зафіксує, чому воно існує. Сприймайте кожне розширювальне поле як технічний борг із радіусом ураження, а не як нешкідливий шум YAML.
| Антипатерн | Що йде не так | Краща альтернатива |
|---|---|---|
Встановлення privileged: true, щоб полагодити одну відмовлену операцію | Контейнер отримує широку могутність рівня хоста замість одного вузького дозволу | Визначте точну потребу в можливості, пристрої чи просторі імен, потім ізолюйте її в переглянутому системному просторі імен |
Прибирання runAsNonRoot після збою запуску | Робоче навантаження повертається до UID 0 і ховає припущення образу про root | Встановіть числовий UID без прав root, виправте володіння файлами чи перезберіть образ із USER без прав root |
| Робити широкі каталоги доступними для запису під кореневою файловою системою лише для читання | Зловмисники можуть змінювати конфігурацію чи бінарні файли в місцях, які рецензенти вважають незмінними | Монтуйте лише ті шляхи даних застосунку, які мають бути з правом запису, і тримайте конфігурацію лише для читання |
Додавання NET_ADMIN, бо мережа заплутана | Pod може змінювати поведінку мережі значно ширше, ніж проста потреба у прив’язці чи з’єднанні | Використовуйте NET_BIND_SERVICE для низьких портів, або перенесіть діагностику в контрольований Pod налагодження |
Один тонкий патерн — відокремити посилення застосунку від посилення агента вузла. Плагіну CNI, драйверу CSI чи демону спостережуваності вузла можуть знадобитися простори імен хоста, пристрої чи підвищені можливості. Це не означає, що ті самі поля прийнятні в просторі імен застосунку. Розміщуйте виняткові робочі навантаження у виділених просторах імен із ясними мітками, вузьким RBAC, винятками політики допуску та слідами перевірки, а потім тримайте звичайні простори імен застосунків узгодженими з обмеженими очікуваннями.
Інший корисний патерн — зробити збої контексту безпеки частиною приймання образу. Якщо сервіс не може запуститися з виконанням без прав root та кореневою файловою системою лише для читання, спитайте, чи не є компонування образу справжнім дефектом. Переміщення шляхів кешу, встановлення володіння файлами під час збирання образу та уникнення привілейованих портів зазвичай роблять маніфест часу виконання простішим. Найкращий контекст безпеки Pod’а часто стає можливим завдяки кращому образу контейнера.
Фреймворк прийняття рішень
Розділ «Фреймворк прийняття рішень»Використовуйте фреймворк прийняття рішень, коли ви не впевнені, чи затягнути поле, додати виняток чи змінити образ. Мета — уникнути обох крайнощів: прийняття небезпечних значень за замовчуванням, бо вони зручні, чи додавання засобів контролю, що ламають робоче навантаження без плану виправлення. Почніть із поведінки застосунку, потім оберіть засіб контролю Kubernetes, що відповідає поведінці. Якщо сама поведінка непотрібна, приберіть поведінку замість надання дозволу.
Start with the failing or unaudited pod | vDoes it need to run as UID 0? |-- no -> set runAsNonRoot + numeric runAsUser |-- yes -> challenge the image design before approving | vDoes it need to write to image paths? |-- no -> set readOnlyRootFilesystem |-- yes -> mount narrow writable volumes or rebuild paths | vDoes it need a Linux capability? |-- no -> drop ALL |-- yes -> drop ALL, add one named capability, document reason | vDoes it need host namespaces or privileged mode? |-- no -> keep them false or unset |-- yes -> move to controlled system namespace with policy reviewДля рішень щодо ідентичності надайте перевагу числовому UID без прав root, що відповідає володінню образу. runAsNonRoot: true ловить помилки, але runAsUser усуває неоднозначність. Якщо образ не має користувача без прав root і всі файли належать root, зміна лише специфікації Pod’а може виявити помилки прав доступу. Це не привід прибирати засіб контролю. Це сигнал перезібрати образ чи змонтувати дані там, де обраний UID може писати.
Для рішень щодо файлової системи спитайте, чи є шлях даними, конфігурацією чи виконуваним контентом. Шляхи даних можуть бути з правом запису, коли застосунок цього потребує. Конфігурація зазвичай має походити з ConfigMap’ів, Secret’ів чи init-згенерованих файлів, змонтованих із вузькими правами. Виконуваний контент має лишатися незмінним. Ця класифікація запобігає поширеній помилці монтування каталогу з правом запису високо в дереві файлової системи через те, що один підшлях дав збій.
Для можливостей починайте з операції, а не з повідомлення про помилку. Прив’язка до порту 80 зіставляється з NET_BIND_SERVICE. Захоплення сирих пакетів зіставляється з ширшими мережевими дозволами і має викликати сильніше питання перевірки. Зміна володіння файлами під час запуску може спокусити вас до CHOWN, але виправлення володіння під час збирання часто чистіше. Список можливостей має пояснювати робоче навантаження, а не компенсувати уникний дизайн образу.
Для seccomp оберіть RuntimeDefault, коли застосунок є звичайним сервісом Linux і немає доказів несумісності системних викликів. Якщо застосунок дає збій під RuntimeDefault, дослідіть, чи справді потрібен заблокований системний виклик. Локальний профіль може дозволити ретельно обраний набір системних викликів, але він потребує розповсюдження на вузли, відстеження версій та тестування на оновленнях середовища виконання. Ці операційні витрати належать до рішення.
Для привілейованого режиму та просторів імен хоста вимагайте іншого стандарту доказів. Команда застосунку, яка каже «це працює лише так», — цього недостатньо. Спитайте, який ресурс хоста потрібен, чи надає його вже спеціалізований агент вузла, чи може процес налагодження впоратися з цим випадком і чи може Pod жити в обмеженому просторі імен. Ці поля наближають робоче навантаження до довіри вузла, тож шлях схвалення має бути явним.
| Потреба робочого навантаження | Налаштування за вибором | Шлях винятку | Питання перевірки |
|---|---|---|---|
| Звичайний HTTP-сервіс на високому порту | UID без прав root, відкинути всі можливості, коренева файлова система лише для читання | Том /tmp чи кешу з правом запису, якщо потрібно | Чи може образ працювати без записів під час запуску, що належать root? |
| HTTP-сервіс на порту 80 | Змінити застосунок на високий порт і відобразити порт Сервісу 80 | Додати NET_BIND_SERVICE після відкидання всіх можливостей | Чи потрібен низький порт усередині контейнера? |
| Пише файли кешу чи тимчасові файли | readOnlyRootFilesystem: true плюс вузькі монтування emptyDir | Персистентний том лише для довговічного стану | Чи є якісь шляхи запису шляхами конфігурації чи виконуваними шляхами? |
| Агент мережі чи сховища рівня вузла | Виділений простір імен та переглянуті підвищені поля | privileged, простори імен хоста чи доступ до пристроїв, як обґрунтовано | Чи є це робоче навантаження системним компонентом, а не застосунком? |
Цей фреймворк також корисний під час реагування на інциденти. Якщо команда пропонує швидку зміну дозволу, зіставте її з таблицею перед застосуванням до виробничого деплойменту. Тимчасовий виняток NET_BIND_SERVICE легше осмислити, ніж тимчасовий привілейований режим. Pod налагодження в контрольованому просторі імен легше прибрати, ніж розширений деплоймент застосунку. Рішення має лишити маніфест усталеного стану безпечнішим, ніж аварійний.
Чи знали ви?
Розділ «Чи знали ви?»Деталі нижче достатньо малі, щоб запам’ятати їх під час іспиту, але достатньо важливі, щоб змінити виробничу перевірку. Кожна з них вказує на поведінку, яка дивує команди, коли контексти безпеки переходять із теорії в реальні робочі навантаження.
runAsNonRootне обирає UID за вас. Якщо образ працює як root, а ви встановлюєтеrunAsNonRoot: trueбез користувача образу без прав root чи явногоrunAsUser, контейнер дає збій замість того, щоб тихо обрати безпечнішого користувача.- Набори можливостей контейнера за замовчуванням усе одно більші, ніж потрібно багатьом застосункам. Поширено задокументований набір за замовчуванням Docker містить 14 можливостей, тому
drop: ["ALL"]— це ясніша базова лінія, ніж аудит однієї можливості за замовчуванням за раз. - Користувач
nobodyзазвичай має UID 65534. Його іноді використовують для виконання без прав root, але UID, специфічний для робочого навантаження, легше пов’язати з володінням образу та правами на томи під час перевірки. - Seccomp стабільний у Kubernetes з версії v1.19. Для робочих навантажень Kubernetes 1.35+
RuntimeDefaultмає бути звичайною відправною точкою, доки тестування не покаже конкретну несумісність.
Типові помилки
Розділ «Типові помилки»Більшість помилок контексту безпеки — це не синтаксичні помилки. Це невідповідності між тим, що обіцяє маніфест, і тим, що насправді робить образ, середовище виконання чи застосунок. Таблиця нижче пов’язує кожну помилку зі звичайною причиною та виправленням, що зберігає намір безпеки.
| Помилка | Чому вона трапляється | Як її виправити |
|---|---|---|
runAsNonRoot без користувача образу без прав root чи runAsUser | Автор очікує, що Kubernetes обере UID, але поле лише валідує, що UID 0 не використовується | Вкажіть числовий runAsUser без прав root або перезберіть образ із USER без прав root та відповідним володінням файлами |
Використання privileged: true для обходу однієї відмовленої операції | Тиск налагодження ховає різницю між одним дозволом та могутністю рівня хоста | Визначте точну потребу в можливості, пристрої чи просторі імен, потім ізолюйте та задокументуйте найменший виняток |
| Невідкидання можливостей | Набір можливостей за замовчуванням невидимий під час звичайного тестування, тож надмірні дозволи здаються нешкідливими | Встановіть capabilities.drop: ["ALL"], додайте назад лише іменовані можливості, що зіставляються з потрібною операцією |
Забуті томи з правом запису при readOnlyRootFilesystem | Образ пише файли кешу, PID, тимчасові чи логів під шляхами, які раніше були з правом запису | Змонтуйте вузькі emptyDir чи персистентні томи за потрібними шляхами даних або змініть образ, щоб писати в інше місце |
| Встановлення лише ідентичності рівня Pod’а | securityContext рівня контейнера може перевизначити поля рівня Pod’а для цього контейнера | Перевірте кожен контейнер і використовуйте політику допуску для відхилення перевизначень на root у просторах імен застосунків |
Додавання широких монтувань із правом запису під /etc чи каталогом застосунку | Швидке виправлення для згенерованої під час запуску конфігурації робить чутливі шляхи доступними для запису під час виконання | Генеруйте конфігурацію в init-контейнері, монтуйте лише згенерований файл чи каталог і надавайте перевагу монтуванням лише для читання |
Сприйняття просторів імен хоста як нешкідливих, бо вони поза securityContext | Рецензенти зосереджуються лише на вкладеному блоці контексту безпеки і пропускають поля просторів імен рівня Pod’а | Проводьте аудит hostPID, hostNetwork, hostIPC, монтувань hostPath та можливостей в одному проході перевірки |
Тест
Розділ «Тест»Використовуйте ці питання як сценарні тренування, а не перевірки на згадування. Блок відповіді пояснює міркування, бо іспит винагороджує швидку діагностику, тоді як робота на проді винагороджує знання того, чому поле є правильним виправленням.
Питання 1: Ваша команда розгортає Pod nginx із `runAsNonRoot: true`, без явного `runAsUser`, та образом, що запускається як root. Pod дає збій ще до появи логів застосунку. Яке безпечне виправлення вам слід зробити?
Залиште runAsNonRoot: true і надайте робочому навантаженню дійсну ідентичність без прав root — або вказавши числовий runAsUser, який може читати файли образу, або використавши образ, що оголошує користувача без прав root. Прибирання runAsNonRoot лише сховало б припущення про root і послабило б робоче навантаження. Якщо новий UID не може писати потрібні шляхи, виправте володіння в образі чи додайте вузькі томи з правом запису, а не повертайтеся до root.
Питання 2: Вебсервіс працює на порту 8080 і має можливості середовища виконання за замовчуванням. Перевірка безпеки просить вас зменшити ризик зловживання на рівні пакетів. Що має робити контекст безпеки?
Відкиньте всі можливості та не додавайте NET_BIND_SERVICE, бо процесу, що слухає на порту 8080, не потрібна можливість прив’язки до низького порту. Поєднайте відкидання можливостей з allowPrivilegeEscalation: false, щоб процес не міг повернути дозволи через шляхи в стилі setuid. Це сильніше, ніж видалення лише NET_RAW, бо це змушує маніфест починати з нульових ambient-можливостей і документує будь-який майбутній виняток.
Питання 3: Ви встановлюєте `readOnlyRootFilesystem: true`, і застосунок дає збій під час запису `/tmp/session` та `/var/log/app/current.log`. Яке виправлення зберігає кореневу файлову систему лише для читання?
Змонтуйте томи з правом запису лише за шляхами, яким потрібні дані часу виконання, як-от /tmp та /var/log/app, або змініть застосунок, щоб логувати в stdout, якщо файлові логи непотрібні. Не вимикайте readOnlyRootFilesystem лише тому, що одному шляху потрібні записи. Також уникайте робити широкі шляхи на кшталт / чи /etc доступними для запису, бо це дає зловмиснику простір для зміни конфігурації чи виконуваного контенту.
Питання 4: Pod має `runAsUser: 1000` на рівні Pod'а, але один сайдкар встановлює `runAsUser: 0` у своєму контексті безпеки контейнера. Який UID працює для сайдкара, і що запобігає цьому обходу в межах простору імен?
Сайдкар працює як UID 0, бо поля рівня контейнера перевизначають поля рівня Pod’а для цього контейнера. Щоб запобігти цьому у спільному просторі імен, використовуйте Pod Security Admission чи інший контролер допуску для відхилення root-контейнерів до того, як вони збережуться. Маніфест робочого навантаження все одно слід виправити, але саме політика зупиняє користувача з правами створення Pod’ів від повторного запису перевизначення.
Питання 5: Розробник просить `privileged: true`, бо діагностичній команді потрібен доступ до сирих пакетів під час збою. Що вам слід перевірити, перш ніж схвалити зміну?
Спершу перевірте, чи може діагностика працювати у виділеному Pod’і налагодження чи контрольованому системному просторі імен, а не розширювати виробничий деплоймент. Потім визначте точну можливість чи доступ до хоста, що потрібні, бо привілейований режим надає набагато більше могутності, ніж лише доступ до сирих пакетів. Якщо виняток справді потрібен, зробіть його обмеженим у часі, переглянутим та ізольованим від звичайних робочих навантажень застосунків.
Питання 6: Посилений Pod дає збій із `operation not permitted` після того, як ви відкинули всі можливості та встановили seccomp `RuntimeDefault`. Як ви вирішуєте, додати можливість чи змінити seccomp?
Починайте з операції, що дала збій, а не з здогаду. Якщо збій — це привілейована мережева операція, операція володіння чи прив’язки до низького порту, зіставте її з іменованою можливістю Linux і вирішіть, чи слід застосунку взагалі робити цю операцію. Якщо логи чи дані аудиту вказують на заблокований системний виклик під seccomp, перевірте, чи RuntimeDefault несумісний із реальною вимогою робочого навантаження, перш ніж розглядати локальний профіль.
Питання 7: Агенту вузла сховища потрібен доступ до пристроїв хоста, тоді як звичайний API-деплоймент у тому самому кластері запитує `hostPID: true` для налагодження. Чи слід обробляти обидва однаково?
Ні. Агент сховища може мати захищувану причину системного рівня для доступу до хоста, але він має жити в контрольованому просторі імен із переглянутими дозволами та винятками допуску. Звичайний API-деплоймент не повинен нести видимість процесів хоста для налагодження. Використовуйте окремий процес налагодження чи тимчасовий діагностичний Pod замість вбудовування доступу до простору імен хоста в деплоймент застосунку.
Практична вправа
Розділ «Практична вправа»У цій вправі ви створите незахищений Pod, заміните його посиленим Pod’ом та переконаєтеся, що посилені налаштування поводяться так, як стверджує маніфест. Команди припускають, що у вас є кластер Kubernetes 1.35+ та робочий контекст kubectl. Вправа використовує nginx, бо він знайомий, але той самий робочий процес застосовується до внутрішніх сервісів після того, як ви врахуєте їхні шляхи запису та володіння образу.
Сценарій вправи: ви перевіряєте сервіс, перш ніж він зможе перейти в простір імен, що забезпечує обмежену (restricted) політику. Поточний Pod не має явного контексту безпеки. Ваше завдання — створити посилену версію, яка працює без прав root, блокує підвищення привілеїв, відкидає всі можливості, потім додає назад NET_BIND_SERVICE, щоб nginx міг прив’язати порт 80, використовує кореневу файлову систему лише для читання та надає томи з правом запису лише там, де nginx їх потребує для цього тесту.
- Завдання 1: Створіть незахищений Pod і перевірте, чи оголошує він якийсь контекст безпеки Pod’а чи контейнера.
- Завдання 2: Застосуйте посилений маніфест Pod’а з ідентичністю без прав root, seccomp
RuntimeDefault, відкинутими можливостями з доданим назадNET_BIND_SERVICEта каталогами часу виконання з правом запису. - Завдання 3: Перевірте збережені поля контексту безпеки рівня Pod’а та рівня контейнера за допомогою
kubectlі підтвердіть, що nginx слухає на порту 80. - Завдання 4: Доведіть, що запис у файлову систему образу дає збій, тоді як запис у
/tmpуспішний. - Завдання 5: Приберіть обидва Pod’и та зафіксуйте, яке поле контексту безпеки пояснило кожну спостережувану поведінку.
Команди розв'язання
# Step 1: Create an insecure pod firstcat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: insecurespec: containers: - name: app image: nginxEOF
# Step 2: Check its security contextkubectl get pod insecure -o yaml | grep -A 20 securityContext# (Likely empty or minimal)
# Step 3: Create hardened versioncat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: hardenedspec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault # Block dangerous kernel syscalls containers: - name: app image: nginx securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL add: - NET_BIND_SERVICE volumeMounts: - name: cache mountPath: /var/cache/nginx - name: run mountPath: /var/run - name: tmp mountPath: /tmp volumes: - name: cache emptyDir: {} - name: run emptyDir: {} - name: tmp emptyDir: {}EOF
# Step 4: Wait for podskubectl wait --for=condition=Ready pod/hardened --timeout=60s
# Step 5: Verify security contextkubectl get pod hardened -o jsonpath='{.spec.securityContext}' | jq .kubectl get pod hardened -o jsonpath='{.spec.containers[0].securityContext}' | jq .
# Step 6: Test that writing to root filesystem failskubectl exec hardened -- touch /etc/test 2>&1 || echo "Write blocked (expected)"
# Step 7: Test that writable volume workskubectl exec hardened -- touch /tmp/test && echo "Write to /tmp succeeded"
# Step 8: Verify nginx bound port 80 with NET_BIND_SERVICE (capability lesson)kubectl exec hardened -- sh -c 'grep -q ":0050" /proc/net/tcp && echo "nginx listening on port 80"'
# Cleanupkubectl delete pod insecure hardenedЯк виглядає успіх
Посилений Pod має зберігати на рівні Pod’а runAsNonRoot, числові налаштування UID та GID, fsGroup та seccompProfile.type: RuntimeDefault. Його контекст безпеки контейнера має показувати allowPrivilegeEscalation: false, readOnlyRootFilesystem: true та набір можливостей, що відкидає ALL, додаючи лише NET_BIND_SERVICE. Nginx має досягти стану Ready і слухати на порту 80 — доводячи, що виняток можливості працює, щойно з’являються шляхи кешу/середовища виконання з правом запису. Запис у /etc/test має дати збій, бо файлова система образу лише для читання, а запис у /tmp/test має бути успішним, бо /tmp підкріплено томом emptyDir.
Найцінніша частина лабораторної — це пояснення, яке ви можете дати опісля. runAsNonRoot та runAsUser контролюють ідентичність процесу. readOnlyRootFilesystem контролює записи в шари образу. emptyDir відновлює записи лише там, де застосунку потрібні дані часу виконання. Відкидання можливостей прибирає ambient-привілеї Linux, тоді як NET_BIND_SERVICE — це єдиний виняток для низького порту, підтверджений тоді, коли /proc/net/tcp показує слухача на порту 80 (:0050). RuntimeDefault додає межу системних викликів, яка не залежить від UID застосунку.
Перевірка засвоєного
Розділ «Перевірка засвоєного»Контексти безпеки — це місце, де ви перетворюєте розпливчасту інструкцію на кшталт «запусти це робоче навантаження безпечно» на конкретні засоби контролю: який UID має виконувати процес, яка група має володіти змонтованими файлами, чи заблоковано підвищення привілеїв, які можливості Linux залишаються та чи доступна коренева файлова система для запису.
Джерела
Розділ «Джерела»- Kubernetes: Configure a Security Context for a Pod or Container
- Kubernetes API reference: Pod securityContext
- Kubernetes API reference: PodSpec host namespace fields
- Kubernetes: Linux kernel security constraints for Pods and containers
- Kubernetes: Seccomp and Kubernetes
- Kubernetes tutorial: Restrict a Container’s Syscalls with seccomp
- Kubernetes: Pod Security Standards
- Kubernetes: Pod Security Admission
- Kubernetes: Enforce Pod Security Standards with namespace labels
- Kubernetes: Mapping PodSecurityPolicies to Pod Security Standards
- Linux manual: capabilities(7)
Наступний модуль
Розділ «Наступний модуль»Модуль 4.2: Pod Security Admission — забезпечення стандартів безпеки на рівні простору імен.