Модуль 3.1: AppArmor для контейнерів
Складність:
[СЕРЕДНЯ]— основа безпеки LinuxЧас на проходження: 40 хвилин
Передумови: основи Linux, знання середовища виконання контейнерів
Що ви зможете зробити
Розділ «Що ви зможете зробити»Після завершення цього модуля ви зможете:
- Спроєктувати профілі AppArmor, які обмежують доступ контейнера до файлів, використання мережі та окремі можливості (capabilities) Linux, не порушуючи нормальний запуск застосунку.
- Застосувати обмеження AppArmor до Подів Kubernetes 1.35+ за допомогою
securityContext.appArmorProfile, водночас розпізнаючи старіший формат анотацій, що використовується в застарілих прикладах. - Діагностувати відмови AppArmor, поєднуючи події Пода, стан профілю на ноді, перевірку
/proc, а також свідчення зdmesgіjournalctl. - Провести аудит конфігурації ноди та робочого навантаження, щоб підтвердити, що кожен запланований контейнер працює під призначеним профілем AppArmor.
Чому цей модуль важливий
Розділ «Чому цей модуль важливий»Гіпотетичний сценарій: контейнеризований вебзастосунок скомпрометовано через помилку в коді, і зловмисник пробує такі звичні кроки: прочитати конфіденційні локальні файли, записати допоміжний бінарний файл у доступний для запису шлях, змінити налаштування на рівні ядра та відкрити мережеві з’єднання, яких застосунок ніколи не потребував. Kubernetes RBAC не зупиняє цю послідовність, тому що зловмисник уже всередині процесу. NetworkPolicy може обмежити частину трафіку, а доступна лише для читання коренева файлова система може заблокувати частину записів, але жоден з них не описує повний набір файлових, мережевих та capability-операцій, які процесу дозволено виконувати на ноді.
AppArmor — це один з інструментів, що звужує цей розрив. Це модуль безпеки Linux (Linux Security Module), який позначає кожну запущену програму іменованим профілем, а потім, для кожної чутливої операції окремо, запитує цей профіль, чи слід її взагалі дозволяти. У роботі з контейнерами цей профіль стає другим периметром навколо процесу. Образ контейнера все ще постачає файлову систему, Kubernetes все ще планує Под, а середовище виконання все ще створює пісочницю, але остаточне рішення «так чи ні» приймає ядро, коли процес звертається до файлів, використовує мережу або запитує можливості Linux.
Для роботи з CKS навичка полягає не в тому, щоб запам’ятати кожне окреме правило AppArmor. Навичка полягає в умінні перекласти операційний намір у невеликий, зрозумілий профіль, завантажити цей профіль на потрібні ноди, застосувати його через сучасний API Kubernetes, а потім довести, що ядро забезпечує саме те, що ви вважаєте, що воно забезпечує. Цей модуль зберігає оригінальні приклади в екзаменаційному стилі, оновлює поверхню Kubernetes до версії 1.35+ та дає вам повторюваний шлях налагодження для тих моментів, коли Под падає через те, що профіль відсутній, виявляється надто суворим або був застосований до неправильного контейнера.
Як AppArmor вписується в захист контейнерів
Розділ «Як AppArmor вписується в захист контейнерів»Про AppArmor найлегше міркувати, якщо відокремити дискреційний контроль доступу (discretionary access control) від мандатного контролю доступу (mandatory access control). Традиційні дозволи Unix запитують лише одне: чи дозволено ідентифікатору користувача та ідентифікатору групи звертатися до конкретного об’єкта. Мандатний контроль доступу додає до цього друге, окреме запитання: навіть якщо процес уже має звичайний дозвіл Unix, чи дозволяє політика, призначена операційною системою, саме цій програмі виконувати саме цю операцію? Це друге запитання особливо важливе для контейнерів, тому що скомпрометований процес часто продовжує працювати з усіма тими звичайними дозволами, які були потрібні його застосунку ще до моменту компрометації.
Середовище виконання контейнерів може вже застосовувати типовий профіль AppArmor, особливо на нодах у стилі Ubuntu. Цей типовий профіль корисний, але він навмисно зроблений загальним, бо має запускати багато різних робочих навантажень, нічого не знаючи про їхню бізнес-логіку. Власний профіль працює інакше: він описує очікувану поведінку одного конкретного робочого навантаження. Статичний вебсервер і збирач логів обидва потребують доступу до файлів і мережі, проте їм потрібні зовсім різні шляхи, різні місця для запису та різна толерантність до привілейованих операцій ядра.
Оригінальну оглядову діаграму варто зберегти, бо вона правильно показує саму точку прийняття рішення: застосунок ніколи не викликає AppArmor напряму. Процес робить системний виклик, ядро доходить до хука AppArmor для відповідної операції, і активний профіль вирішує, чи продовжувати цю операцію. Практичний наслідок полягає в тому, що відмова AppArmor може виглядати як звичайна помилка застосунку, наприклад «permission denied», навіть якщо біти режиму файлу Unix виглядають дозвільними.
+-----------------------------------------------------------+| APPARMOR OVERVIEW |+-----------------------------------------------------------+| || AppArmor = Application Armor || ------------------------------------------------------- || - Mandatory Access Control (MAC) system || - Restricts per-program capabilities || - Default on Ubuntu, Debian || - Alternative to SELinux on many RHEL-family systems || || How it works: || || +-----------------+ +-----------------+ || | Application | --> | System Call | || | (Container) | | | || +-----------------+ +--------+--------+ || | || v || +-----------------+ || | AppArmor | || | Profile Check | || +--------+--------+ || | || +------------+------------+ || v v || +---------+ +---------+ || | ALLOWED | | DENIED | || +---------+ +---------+ || |+-----------------------------------------------------------+Режим профілю змінює поведінку цієї точки прийняття рішення. Режим примусу (enforce) блокує операції, які профіль не дозволяє, та фіксує відмову. Режим скарг (complain) фіксує те, що було б відхилено, але дозволяє застосунку продовжувати роботу. Вимкнене (disabled) або необмежене (unconfined) виконання видаляє цей профіль з процесу, що може бути прийнятним для короткого діагностичного порівняння, але не має бути сталим станом для виробничого робочого навантаження, яке можна обмежити.
+-----------------------------------------------------------+| APPARMOR PROFILE MODES |+-----------------------------------------------------------+| || Enforce Mode || - Policy is enforced, violations are blocked and logged || aa-enforce /path/to/profile || || Complain Mode || - Policy violations are logged but not blocked || aa-complain /path/to/profile || Useful for testing new profiles || || Disabled / Unconfined || - No AppArmor restrictions applied || |+-----------------------------------------------------------+Режим скарг — це саме те місце, з якого починаються багато успішних профілів. Ви розміщуєте профіль навколо реалістичного робочого навантаження, виконуєте звичайні для нього дії, уважно читаєте зафіксовані потенційні відмови та лише потім вирішуєте, чи дійсно застосунок потребує кожної з цих операцій. Такий робочий процес помітно повільніший, ніж просто написати правило заборони та сподіватися на краще, але він дає змогу уникнути небезпечного компромісу: профілю, який виглядає суворим і надійним у маніфесті, а потім не дає застосунку навіть запуститися під реальним трафіком.
Зробіть паузу та спрогнозуйте: якщо новий профіль забороняє запис у /etc/**, чого ви очікуєте від образу, що генерує файли конфігурації в /etc/nginx/conf.d/ перед запуском nginx? Важлива відповідь — не просто «він може впасти». Важлива відповідь полягає в тому, що відмова спершу проявиться на рівні застосунку, тоді як причина, якій ви довіряєте, перебуває в журналі ядра, де AppArmor фіксує заблокований запис.
Ви можете перевірити ноду, перш ніж торкатися Пода. Перша команда перевіряє, чи увімкнено модуль ядра. Друга запитує в інструментарію AppArmor зведення. Третя підтверджує, що ядро завантажило профілі, зареєстровані для примусу. У середовищі в стилі CKS ці перевірки також застерігають вас від налагодження маніфесту Kubernetes, коли справжня проблема в тому, що нода не може забезпечити профіль. Лабораторні ноди CKS і kind орієнтовані передусім на containerd; docker часто відсутній, тому не покладайтеся на docker info для перевірки підтримки AppArmor.
# Check if AppArmor is enabled (kernel module)cat /sys/module/apparmor/parameters/enabled# Output: Y (enabled) or N (disabled)
# Check AppArmor status (loaded profiles and modes)sudo aa-status
# Output example:# apparmor module is loaded.# 39 profiles are loaded.# 38 profiles are in enforce mode.# /usr/bin/evince# /usr/sbin/cupsd# docker-default# 1 profiles are in complain mode.# 10 processes have profiles defined.
# List loaded profilessudo aa-status --profiles
# containerd / CRI nodes: confirm profiles are visible to the kernel# (no docker CLI required)sudo grep -E '^profile ' /sys/kernel/security/apparmor/profiles | head# Or list profile names from aa-statussudo aa-status --profilesТиповий профіль середовища виконання контейнерів — це базовий рівень, а не політика робочого навантаження. Docker історично називав свій типовий профіль docker-default, а інсталяції containerd часто надають типовий профіль середовища виконання через Kubernetes як RuntimeDefault. Ці профілі зазвичай блокують небезпечні операції, такі як монтування файлових систем, запис у чутливі шляхи /proc та використання сирих мережевих можливостей, водночас дозволяючи достатньо дій, щоб звичайні контейнери запускалися.
# Docker/containerd use a runtime default profile# This profile commonly:# - Denies mounting filesystems# - Denies access to /proc/sys# - Denies raw network access# - Allows normal container operations
# Check a Docker default profile location when presentcat /etc/apparmor.d/containers/docker-default 2>/dev/null || \ cat /etc/apparmor.d/docker 2>/dev/nullТиповий профіль — правильний перший крок, коли в робочого навантаження немає причин працювати необмежено. Власний профіль — наступний крок, коли ви можете описати жорсткіший намір, наприклад «цей контейнер може читати більшість файлів, писати лише в /tmp і ніколи не відкривати мережевий сокет». Розглядайте ці власні профілі як активи ноди. Kubernetes посилається на профіль за іменем, але ядро на вибраній ноді має вже знати це ім’я, перш ніж kubelet зможе запустити з ним контейнер.
Проєктування профілів, що відповідають поведінці контейнера
Розділ «Проєктування профілів, що відповідають поведінці контейнера»Профіль AppArmor — це, по суті, контракт між очікуваною поведінкою застосунку та тими операціями ядра, які ви будете готові терпіти. Хороші профілі починаються з вузького, чіткого формулювання призначення: цей процес читає свої запаковані ресурси, пише тимчасові файли, обслуговує HTTP і ніколи не потребує ані читати бази даних облікових записів, ані переписувати налаштування ядра на рівні ноди. Погані профілі починаються зі скопійованого звідкись списку заборон, без жодної моделі застосунку за ним, а це означає лише одне: перший же виробничий сплеск трафіку перетворюється на справжній набір тестів.
Профілі зберігаються та завантажуються на кожній ноді, а не всередині API Kubernetes. Цю відмінність легко пропустити, бо маніфест Пода виглядає як місце, де живе рішення про безпеку. Маніфест лише іменує профіль. Вміст профілю локальний для хоста, його розбирає інструментарій AppArmor і реєструє в ядрі. Якщо дві ноди мають різні файли в /etc/apparmor.d/, той самий маніфест Пода може поводитися по-різному після перепланування.
# AppArmor profiles are stored in:/etc/apparmor.d/
# For Kubernetes, create profiles in:/etc/apparmor.d/# Profile must be loaded on each node where pod might runНаведений нижче кістяк профілю навмисно невеликий, але репрезентативний. Він містить глобальні налаштування (tunables), базові абстракції, файлові правила, мережеві правила, capability-правила та явні правила заборони. Прапорці attach_disconnected і mediate_deleted зазвичай використовуються для профілів контейнерів, бо контейнери можуть надавати шляхи, які з погляду ядра не виглядають як звичайні шляхи хоста, особливо навколо просторів імен монтування та видалених, але все ще відкритих файлів.
#include <tunables/global>
profile my-container-profile flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
# File access rules /etc/passwd r, # Read /etc/passwd /var/log/myapp/** rw, # Read/write to log directory /tmp/** rw, # Read/write to tmp
# Network rules network inet tcp, # Allow TCP network inet udp, # Allow UDP
# Capability rules capability net_bind_service, # Allow binding to ports < 1024
# Deny rules deny /etc/shadow r, # Deny reading shadow deny /proc/sys/** w, # Deny writing to /proc/sys}Синтаксис виглядає компактним, бо AppArmor спроєктовано так, щоб його читали як політику, а не як програмний код. Шлях, за яким стоїть r, надає доступ на читання. Шлях, за яким стоїть w, надає доступ на запис. Кінцевий /** застосовується рекурсивно під каталогом. Мережеві та capability-правила використовують власні простори імен, тому network inet tcp, — це не файловий шлях, а capability net_bind_service, — не команда Linux.
+-----------------------------------------------------------+| APPARMOR RULE SYNTAX |+-----------------------------------------------------------+| || File Access: || ------------------------------------------------------- || /path/to/file r, # Read || /path/to/file w, # Write || /path/to/file rw, # Read and Write || /path/to/file a, # Append || /path/to/file ix, # Execute, inherit profile || /path/to/dir/ r, # Read directory || /path/to/dir/** rw, # Recursive read/write || || Network: || ------------------------------------------------------- || network, # Allow all networking || network inet, # IPv4 || network inet6, # IPv6 || network inet tcp, # IPv4 TCP only || network inet udp, # IPv4 UDP only || || Capabilities: || ------------------------------------------------------- || capability dac_override, # Bypass file permissions || capability net_admin, # Network admin || capability sys_ptrace, # Trace processes || || Deny: || ------------------------------------------------------- || deny /path/file w, # Explicitly deny and log || |+-----------------------------------------------------------+Наступний профіль зберігає оригінальний приклад заборони запису з модуля. Він дозволяє загальне читання файлів через правило file,, а потім явно забороняє запис усюди, крім /tmp. Така форма корисна для навчальної лабораторії, бо одна команда touch /etc/test має завершитися невдало, тоді як touch /tmp/test все ще має успішно виконатися. У реальних робочих навантаженнях ви ще більше затягнули б файлові правила, поспостерігавши, що насправді читає процес.
# Create profile on each nodesudo tee /etc/apparmor.d/k8s-deny-write << 'EOF'#include <tunables/global>
profile k8s-deny-write flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
# Allow most read operations file,
# Deny all write operations except /tmp deny /** w, /tmp/** rw,
# Allow network network,}EOFЗавантаження профілю — це операція на ноді. Парсер перевіряє синтаксис профілю та реєструє профіль у ядрі. Прапорець -r замінює наявну завантажену версію файлом, який ви надаєте, що зручно під час ітеративної розробки. Команда видалення так само важлива в лабораторії, бо застарілі профілі можуть заплутати пізніші тести, якщо Под продовжує посилатися на старе ім’я.
# Parse and load the profilesudo apparmor_parser -r /etc/apparmor.d/k8s-deny-write
# Verify it's loadedsudo aa-status | grep k8s-deny-write
# To remove a profilesudo apparmor_parser -R /etc/apparmor.d/k8s-deny-writeПерш ніж це запустити, якого виводу ви очікуєте від sudo aa-status | grep k8s-deny-write після успішного завантаження, і що б ви перевірили, якби він нічого не вивів? Корисний поділ для налагодження — це синтаксис проти реєстрації. Синтаксична помилка має з’явитися, коли виконується apparmor_parser, тоді як відсутній профіль після чистого запуску парсера зазвичай вказує на неправильну ноду, неправильний шлях до файлу або на те, що AppArmor не увімкнено на цьому хості.
Більшість помилок у профілях походить від надання та заборони на неправильному рівні деталізації. Заборона /** w, потужна, але вона може зламати кеші пакетів, PID-файли, згенеровану конфігурацію, сокети середовища виконання або шляхи логів застосунку. Дозвіл file, зручний, але він може бути ширшим, ніж має бути фінальний виробничий профіль. Сенс першого чернетки — не досконалість; це вимірювана політика, яку ви можете випробувати, поспостерігати та вдосконалити.
Застосування профілів у Kubernetes 1.35+
Розділ «Застосування профілів у Kubernetes 1.35+»Kubernetes 1.35+ має використовувати securityContext.appArmorProfile, а не старішу анотацію AppArmor. API приймає три типи профілів: RuntimeDefault, Localhost та Unconfined. RuntimeDefault просить середовище виконання застосувати свій типовий профіль, Localhost іменує профіль, уже завантажений на ноді, а Unconfined навмисно знімає обмеження AppArmor. Для практики CKS саме тип профілю Localhost перевіряє, чи розумієте ви як крок на ноді, так і крок на Поді.
Сучасний еквівалент оригінального прикладу secured-pod застосовує профіль k8s-deny-write на рівні контейнера. Конфігурація на рівні контейнера явна та уникає неоднозначності в багатоконтейнерних Подах, де одному sidecar може знадобитися інша політика, ніж основному застосунку. securityContext.appArmorProfile на рівні Пода може задати типове значення для кожного контейнера, але значення на рівні контейнера має пріоритет, коли присутні обидва.
Контейнери ініціалізації (init), sidecar-контейнери та ефемерні діагностичні контейнери заслуговують на таку саму увагу, як і основний контейнер застосунку. Профіль на рівні Пода може надати типове значення, але контейнер, якому потрібна інша поведінка, має вказати власний профіль безпосередньо. Це важливо під час налагодження, бо ефемерний контейнер, що працює з ширшим профілем, ніж застосунок, який впав, може приховати відмову, яку ви намагаєтеся дослідити. Порівнюйте позначки профілю для процесу, який ви насправді тестуєте.
apiVersion: v1kind: Podmetadata: name: secured-podspec: containers: - name: app image: nginx securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-deny-writeСтаріша форма анотації збережена тут, бо багато старих нотаток, старих екзаменаційних описів та старіших кластерів усе ще її показують. У документації Kubernetes ця форма описана як API до версії v1.30. У сучасному кластері надавайте перевагу структурованому полю вище; тримайте форму анотації в пам’яті лише для того, щоб розпізнавати застарілі маніфести та мігрувати їх, не помилившись у прочитанні посилання на профіль.
apiVersion: v1kind: Podmetadata: name: secured-pod annotations: # Legacy format used before Kubernetes v1.30: # container.apparmor.security.beta.kubernetes.io/<container-name>: <profile> container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-deny-writespec: containers: - name: app image: nginxЗначення посилання на профіль змінили форму, коли Kubernetes переніс AppArmor у securityContext. Стара анотація кодувала і джерело, і ім’я в одному рядку, наприклад localhost/k8s-deny-write. Сучасний API розділяє джерело на type: Localhost, а ім’я на localhostProfile: k8s-deny-write. Такий поділ простіший для політики допуску та інструментів, а також робить недопустимі комбінації очевиднішими.
# Kubernetes 1.35+ structured field:securityContext: appArmorProfile: type: RuntimeDefault
securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-deny-write
securityContext: appArmorProfile: type: Unconfined
# Legacy annotation format:container.apparmor.security.beta.kubernetes.io/<container-name>: <profile-ref>
# Legacy profile reference options:# runtime/default - Use container runtime's default profile# localhost/<name> - Use profile loaded on node with <name># unconfined - No AppArmor restrictionsЩо сталося б, якби ви створили профіль AppArmor і завантажили його на node-1, але не на node-2, а потім дозволили Поду без nodeSelector переміститися під час спорожнення (drain)? Планувальник не розуміє, які профілі AppArmor завантажено на яких нодах. Kubelet на вибраній ноді виконує остаточну перевірку, тому Под може працювати на одній ноді й бути відхиленим на іншій через те, що іменований локальний профіль відсутній.
Така поведінка створює операційне правило: власні профілі — це артефакти розгортання в масштабі кластера, а не одноразові файли. Якщо кожна робоча нода може запускати Под, кожна робоча нода потребує завантаженого профілю до застосування маніфесту. Якщо профіль є лише на частині нод, позначте ці ноди мітками та обмежте робоче навантаження ними. Без такої дисципліни подія перепланування може перетворити успішну зміну безпеки на інцидент доступності.
AppArmor також взаємодіє з Pod Security Admission. За рівня Pod Security Baseline (і, відповідно, Restricted) профілі Unconfined AppArmor відхиляються під час допуску; профілі Localhost, відсутні на ноді, усе одно завершуються невдало на kubelet. Це означає, що конфігурація AppArmor — не лише турбота ноди: допуск може заблокувати небезпечні типи профілів ще до планування, тоді як завантаження локальних профілів залишається вашою відповідальністю для посилань Localhost.
Аудит запущеного контейнера має включати погляд ядра, а не лише YAML, який ви подали. Надійна перевірка — прочитати /proc/1/attr/current усередині контейнера. Якщо вивід каже k8s-deny-write (enforce), кореневий процес працює під цим профілем. Якщо він каже про типовий профіль середовища виконання, неочікуваний профіль або unconfined, ваш маніфест, стан ноди чи конфігурація середовища виконання не відповідають вашому наміру.
# Verify the active AppArmor label from inside the containerkubectl exec secured-pod -- cat /proc/1/attr/current
# Check pod events for profile load or admission problemskubectl describe pod secured-pod | grep -i apparmor
# Confirm the profile exists on a specific nodessh node1 'sudo aa-status | grep k8s-deny-write'Найпоширеніше рішення в Kubernetes — обрати між RuntimeDefault та власним профілем Localhost. Використовуйте RuntimeDefault, коли вам швидко потрібен широкий базовий рівень, а робоче навантаження ще не профільоване. Використовуйте Localhost, коли поведінка застосунку зрозуміла достатньо, щоб власна політика могла прибрати суттєвий доступ. Уникайте Unconfined, окрім тимчасового діагностичного засобу, і знімайте його, щойно порівняння завершено.
Розгортання на нодах і контроль дрейфу
Розділ «Розгортання на нодах і контроль дрейфу»Власні профілі AppArmor створюють проблему життєвого циклу, якої просто немає у звичайних об’єктів Kubernetes. Деплоймент, ConfigMap чи Secret зберігаються на API-сервері та узгоджуються контролерами автоматично. Натомість профіль AppArmor Localhost — це водночас файл на диску й реєстрація в ядрі на кожній окремій ноді. Kubernetes може посилатися на його ім’я, але він не поширює сам файл, не перезавантажує парсер і ніяк не доводить, що кожна придатна нода має той самий вміст профілю. Саме тому контроль дрейфу (drift) стає невіддільною частиною дизайну безпеки.
Є чотири поширені способи поширення профілів. Ви можете запекти їх в образ ноди, встановити їх засобами керування конфігурацією, завантажити їх привілейованим DaemonSet або створити їх вручну в короткотривалому екзаменаційному середовищі. Запікання в образ передбачуване для керованих парків, бо ноди-заміни стартують уже з присутнім профілем. Керування конфігурацією гнучке для довгоживучих хостів. DaemonSet привабливий у командах, орієнтованих на Kubernetes, але він має бути достатньо привілейованим, щоб писати файли хоста та запускати парсер, а це означає, що він заслуговує на ретельний огляд.
Ручне завантаження профілю цілком прийнятне в лабораторії CKS, бо середовище там невелике, а завдання обмежене в часі. Проте це поганий виробничий патерн, бо нова нода, ремонт наявної ноди чи подія автомасштабувальника можуть тихо й непомітно прибрати те припущення, яке власне й змусило маніфест працювати. Відмінність тут не в тому, чи правильні самі команди. Вона в тому, чи має організація надійний механізм, який знову зробить правильний стан істинним після того, як нода зміниться.
Перша категорія дрейфу — відсутній профіль. Файлу може не існувати, парсер міг ніколи не запуститися, або профіль міг бути прибраний після очищення. Друга категорія — застарілий вміст, коли ім’я існує, але його правила відрізняються від версії, проти якої тестувалося робоче навантаження. Третя категорія — дрейф режиму, коли одна нода має профіль у режимі скарг, а інша примушує його. Четверта категорія — дрейф можливостей ноди, коли сам AppArmor вимкнено або не підтримується.
Ви можете перевірити ці категорії простими командами хоста перед налагодженням робочого навантаження. Контрольна сума доводить, чи відповідає вміст файлу очікуваній версії. aa-status доводить, чи зареєстрований профіль і в якому режимі він перебуває — примусу чи скарг. Перевірка модуля ядра доводить, чи може хост узагалі примушувати AppArmor. Жодна з цих перевірок не залежить від маніфесту Пода, тому вони корисні, коли kubelet відхиляє контейнер ще до його повного запуску.
PROFILE=/etc/apparmor.d/k8s-deny-write
# Check whether the node can enforce AppArmorcat /sys/module/apparmor/parameters/enabled
# Check the local profile file contentsudo sha256sum "$PROFILE"
# Check whether the profile is loaded and which mode it usessudo aa-status | grep k8s-deny-writeМітки нод — практичний місток між локальним станом профілю на ноді та плануванням Kubernetes. Якщо профіль є лише на частині нод, позначте ці ноди мітками та використайте nodeSelector або спорідненість нод (node affinity) у робочому навантаженні. Мітка не завантажує профіль і не доводить, що вміст профілю правильний. Вона просто не дає планувальнику надсилати Под на ноди, які ви не оголосили придатними для цього профілю.
kubectl label node node-1 apparmor.kubedojo.io/k8s-deny-write=truekubectl label node node-2 apparmor.kubedojo.io/k8s-deny-write=trueapiVersion: v1kind: Podmetadata: name: secured-podspec: nodeSelector: apparmor.kubedojo.io/k8s-deny-write: "true" containers: - name: app image: nginx securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-deny-writeМітку планування слід розглядати як твердження, яке потрібно підтримувати, а не як декоративний тег. Якщо нода втрачає профіль під час оновлення, але зберігає мітку, робоче навантаження все одно може потрапити туди й завершитися невдало. Якщо нода має профіль, але їй бракує мітки, робоче навантаження може мати менше потужності, ніж очікувалося. У виробництві та сама автоматизація, що завантажує профіль, має встановлювати або перевіряти мітку планування після успішної реєстрації профілю.
Версіонування імен профілів може зробити зміни безпечнішими. Замість редагування k8s-web на місці, поки Поди ще працюють, створіть k8s-web-v2, завантажте його скрізь, оновіть одне тестове робоче навантаження, щоб воно посилалося на нове ім’я, та поспостерігайте за результатом. Після успішного розгортання перемістіть решту Подів і виведіть з експлуатації старий профіль. Це коштує трохи більше обліку, але уникає важкого для налагодження стану, де одне ім’я профілю означає різну політику на різних нодах під час розгортання.
Зміни режиму заслуговують на таку саму обережність, як і зміни правил. Профіль у режимі скарг може здаватися «робочим», бо застосунок продовжує працювати, проте логи показують операції, які завершилися б невдало в режимі примусу. Профіль у режимі примусу може бути надто суворим, але швидко виявить проблему. Коли ви переходите від скарг до примусу, запишіть поведінку робочого навантаження, яку ви випробували. Тестування лише на старті недостатнє, якщо процес пише файли під час ротації, перезавантажує конфігурацію за сигналом або відкриває мережеві сокети лише під час запланованого завдання.
# Put a profile into complain mode while observing normal behaviorsudo aa-complain /etc/apparmor.d/k8s-deny-write
# Switch the same profile back to enforce mode after reviewsudo aa-enforce /etc/apparmor.d/k8s-deny-writeБагатоконтейнерні Поди додають ще одну складність розгортання. Профіль AppArmor на рівні Пода зручний, коли всі контейнери можуть мати спільне обмеження, але sidecar-контейнери часто поводяться інакше, ніж основні. Проксі-sidecar може потребувати мережевого доступу, який не потрібен основному контейнеру обробки файлів. Init-контейнер може писати файли, які пізніше читає застосунок. Якщо ви застосовуєте один профіль на рівні Пода, протестуйте шлях кожного контейнера, а не лише основного процесу.
Для аудитів фіксуйте свідчення так, щоб відрізнити бажану політику від спостережуваного стану. Бажана політика — це поле маніфесту, файл-джерело профілю та запис релізу. Спостережуваний стан — це контрольна сума на ноді, режим завантаженого профілю, історія подій Пода та /proc/1/attr/current з контейнера. Рецензент безпеки має змогти простежити від «ми мали намір застосувати k8s-deny-network» до «цей процес наразі позначено k8s-deny-network (enforce)», не покладаючись на пам’ять чи скриншоти.
Перш ніж змінювати профіль, запитайте, які робочі навантаження наразі на нього посилаються. Kubernetes не веде першокласного індексу імен профілів, тому ви зазвичай шукаєте в маніфестах, значеннях Helm, репозиторіях GitOps та живих Подах. Жива перевірка може виявити аварійні правки, які так і не повернулися в систему контролю версій. Перевірка джерела може виявити робочі навантаження, які сьогодні масштабовані до нуля, але використовуватимуть профіль пізніше. Обидва погляди важливі, коли ви видаляєте чи перейменовуєте профіль.
# Search live pod specs for AppArmor profile referenceskubectl get pods --all-namespaces -o yaml | grep -i apparmor -C 3
# Search local manifests in a repositoryrg -n "appArmorProfile|container.apparmor.security.beta.kubernetes.io" .Екзаменаційна версія цього робочого процесу менша, але слідує тій самій логіці. Завантажте профіль на ноду, яку ви будете використовувати, перевірте його через aa-status, зробіть так, щоб Под потрапив туди, де профіль існує, і доведіть примус зсередини контейнера. Якщо завдання надає конкретне ім’я ноди, виконуйте роботу з нодою там. Якщо ні — або завантажте профіль на кожну робочу ноду, або навмисно обмежте Под після того, як ви позначили підготовлену ноду міткою.
Для довгоживучих кластерів включайте AppArmor у планування заміни нод. Автомасштабовані ноди, відремонтовані віртуальні машини та перезібрані bare-metal хости можуть приєднуватися до кластера без локальних профілів, які припускають старіші робочі навантаження. Хороша перевірка готовності для пулу захищених нод має включати підтримку ядра, очікувані контрольні суми профілів, статус режиму примусу та мітки планування, що рекламують ці профілі. Така перевірка перетворює AppArmor з ручного налаштування безпеки на умову придатності ноди.
Це мислення про розгортання запобігає оманливій умові успіху. Под, що запустився одного разу, не є доказом готовності кластера до AppArmor. Сильніша умова успіху каже, що кожна придатна нода може примушувати іменований профіль, робоче навантаження за потреби обмежене цими нодами, запущений контейнер має призначену позначку, а логи відмов відповідають політиці, яку ви хотіли протестувати. У цьому й полягає різниця між робочою демонстрацією та підтримуваним засобом контролю безпеки.
Операційне розгортання та налагодження
Розділ «Операційне розгортання та налагодження»Найбезпечніший патерн розгортання складається з чотирьох кроків: спостерігати, обмежувати, перевіряти, а вже потім розширювати розгортання. Почніть з профілю в режимі скарг або у вузькому тестовому середовищі, ретельно випробуйте робоче навантаження, перегляньте логи й скоригуйте правила за результатами. Потім перемкніться в режим примусу та повторіть те саме робоче навантаження ще раз. І лише після того, як примус успішно спрацював на одній ноді, поширте профіль на кожну ноду, що може запускати цей Под, і тільки тоді застосуйте або оновіть маніфест робочого навантаження.
Наведені нижче збережені профілі показують три корисні родини політик. Перша блокує запис у кореневу файлову систему, окрім тимчасових місць середовища виконання. Друга блокує мережевий доступ для робочих навантажень, які ніколи не мають виходити назовні. Третя забороняє читання чутливих файлів облікових записів і привілеїв. Це лабораторні патерни, а не універсальні виробничі профілі, але вони навчають форми правила, яке можна вдосконалити для конкретного процесу.
#include <tunables/global>
profile k8s-readonly flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
# Read everything /** r,
# Write only to specific paths /tmp/** rw, /var/tmp/** rw, /run/** rw,
# Deny write elsewhere deny /** w,
network,}Патерн «лише для читання кореневого розділу» корисний, коли образ контейнера має бути незмінним під час виконання, але процесу все ще потрібен робочий простір. Він добре поєднується з readOnlyRootFilesystem: true, проте не тотожний йому. Налаштування Kubernetes змінює поведінку монтування кореневої файлової системи контейнера, тоді як AppArmor може опосередковувати додаткові файлові шляхи та робити відмову видимою в журналі аудиту ядра.
#include <tunables/global>
profile k8s-deny-network flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
file,
# Deny all network access deny network,}Патерн заборони мережі навмисно прямолінійний. Він чудово підходить для пакетного завдання, яке лише перетворює локальний ввід, або для sidecar, який має спілкуватися через спільні файли, а не сокети. Він погано пасує для вебсервера, DNS-клієнта, інсталятора пакетів чи експортера телеметрії. Коли ви бачите deny network,, одразу запитайте, чи створюють сокети стартові скрипти, перевірки працездатності (health checks) або код бібліотек, перш ніж запуститься основний застосунок.
#include <tunables/global>
profile k8s-deny-sensitive flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
file, network,
# Deny access to sensitive files deny /etc/shadow r, deny /etc/gshadow r, deny /etc/sudoers r, deny /etc/sudoers.d/** r, deny /root/** rwx,}Патерн чутливих файлів — хороший навчальний приклад, бо він блокує поведінку, якої майже жоден контейнер застосунку не повинен потребувати. Будьте обережні, проте, не узагальнюйте його до «заборонити весь /etc». Багато застосунків читають набори CA-сертифікатів, конфігурацію резолвера, монтування сервісного акаунта або конфігурацію застосунку під шляхами, які на перший погляд виглядають чутливими. Старший рецензент профілю запитує, чи кожен заборонений шлях шкідливий і чи кожен дозволений шлях необхідний.
Зробіть паузу та спрогнозуйте: ви застосовуєте профіль AppArmor із deny /etc/** w, до контейнера, що працює з nginx, а цей образ пише згенеровану конфігурацію під /etc/nginx/conf.d/ під час старту. Под може заплануватися, контейнер може почати запускатися, а потім процес може впасти, коли запис заборонено. Вашою наступною перевіркою має бути журнал ядра, а не чергова сліпа правка маніфесту.
Оригінальний сценарій у стилі CKS для застосування наявного профілю використовував застарілу анотацію. У сучасному кластері перекладіть намір на appArmorProfile; у старішому екзаменаційному середовищі розпізнайте, чому ключ анотації має точно містити ім’я контейнера. Захищений приклад залишається нижче як довідник сумісності, бо він фіксує стару пастку: анотація іменує контейнер, а не образ.
# Check if profile is loadedsudo aa-status | grep my-profile
# Legacy annotation example for older clusterscat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: test-pod annotations: container.apparmor.security.beta.kubernetes.io/nginx: localhost/my-profilespec: containers: - name: nginx image: nginxEOFДругий збережений сценарій створює та застосовує профіль, що забороняє запис під /etc. Операційний урок усе ще актуальний, навіть якщо поверхня маніфесту змінилася: напишіть профіль, завантажте його, застосуйте його до контейнера та перевірте операцією, яка має завершитися невдало. У Kubernetes 1.35+ ви замінили б блок анотації структурованим полем, показаним раніше, але авторство профілю та тест на відмову залишаються тими самими.
# Create profile that denies write to /etcsudo tee /etc/apparmor.d/k8s-deny-etc-write << 'EOF'#include <tunables/global>
profile k8s-deny-etc-write flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> file, network, deny /etc/** w,}EOF
# Load profilesudo apparmor_parser -r /etc/apparmor.d/k8s-deny-etc-write
# Kubernetes 1.35+ — apply with securityContext.appArmorProfilecat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: secured-nginxspec: containers: - name: nginx image: nginx securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-deny-etc-writeEOF
# Wait for pod to be readykubectl wait --for=condition=Ready pod/secured-nginx --timeout=60s
# Verifykubectl exec secured-nginx -- touch /etc/test# Should fail due to AppArmorЛише для розпізнавання застарілого формату (виведено з ужитку з v1.30; видає попередження на v1.35 — не використовуйте в нових маніфестах):
apiVersion: v1kind: Podmetadata: name: secured-nginx annotations: container.apparmor.security.beta.kubernetes.io/nginx: localhost/k8s-deny-etc-writespec: containers: - name: nginx image: nginxНалагодження проблем AppArmor — це багаторівневий процес. Події Пода кажуть вам, чи відхилили посилання на профіль Kubernetes або kubelet. Команди ноди кажуть, чи існує профіль і чи увімкнено AppArmor. Журнали ядра кажуть, яка саме операція, профіль, процес, шлях та запитувана маска спричинили відмову. Вам потрібні всі три рівні, бо будь-який один рівень сам по собі може спрямувати вас у неправильному напрямку.
# Check pod eventskubectl describe pod secured-pod | grep -i apparmor
# Check if profile is loaded on nodessh node1 'sudo aa-status | grep k8s'
# Check audit logs for denialssudo dmesg | grep -i apparmor | tail -10
# Or check audit logsudo journalctl -k | grep -i apparmorКоли відмова з’являється в dmesg чи journalctl, читайте її як свідчення, а не як шум. Ім’я профілю каже вам, яка політика була активною. Операція описує, що намагався зробити процес, наприклад читання файлу, запис файлу, виконання, mmap або мережу. Запитувана та відхилена маски показують, який дозвіл було заблоковано. Шлях дає вам наступне правило для перегляду, але він автоматично не доводить, що застосунку слід дозволити використовувати цей шлях.
Поширена пастка усунення несправностей — послаблювати профіль, доки симптом не зникне, а потім зупинятися. Такий підхід перетворює профіль на повільний спосіб заново відкрити unconfined. Натомість зафіксуйте відхилену операцію, зіставте її з поведінкою застосунку та вирішіть, чи є ця операція частиною очікуваного контракту. Якщо вона очікувана, додайте найвужче правило дозволу, що її підтримує. Якщо вона неочікувана, збережіть відмову та виправте застосунок, стартовий скрипт чи образ.
Сценарій-вправа: Под успішно працює кілька днів, а потім падає після оновлення ноди, навіть попри те, що маніфест не змінювався. AppArmor має бути у вашому короткому списку, якщо контейнер повідомляє про помилки дозволу під час старту, активний профіль відрізняється на різних нодах або aa-status показує відсутній профіль на оновленій ноді. Маніфест не змінився, але змінився локальний актив безпеки на ноді, а нода — це місце, де AppArmor насправді живе.
Патерни та антипатерни
Розділ «Патерни та антипатерни»Найсильніший патерн AppArmor — розглядати профілі як конфігурацію ноди з версіонуванням, а не як разові файли. Тримайте текст профілю в системі контролю версій, розгортайте його контрольованим, передбачуваним шляхом керування нодами та робіть так, щоб робоче навантаження посилалося на ім’я профілю, яке існує всюди, куди воно взагалі може плануватися. Це працює, бо перетворює локальну залежність від ядра на придатний для аудиту й повторюваний артефакт релізу. У масштабі поєднуйте це з мітками нод лише в тих випадках, коли профіль навмисно обмежено меншим пулом нод.
Другий патерн — починати з RuntimeDefault і резервувати власні профілі Localhost для робочих навантажень, де додаткове обмеження конкретне й піддається тестуванню. Суцільний власний профіль для кожного сервісу звучить дисципліновано, але часто породжує поверхневі копії, яких ніхто не розуміє. Типові профілі середовища виконання дають широке базове покриття одразу, тоді як власні профілі приносять користь, коли вони кодують реальні відмінності між вебсервером, пакетним обробником та завданням обробки файлів.
Третій патерн — перевіряти примус зсередини контейнера та з ноди. Перевірка /proc/1/attr/current з боку контейнера доводить, яку позначку насправді несе процес. Перевірка aa-status або /sys/kernel/security/apparmor/profiles з боку ноди доводить, що хост знає профіль. Маніфест Пода доводить лише намір. Робота з безпекою потребує згоди наміру, стану хоста та результату виконання.
Найспокусливіший антипатерн — встановити Unconfined, щоб запустити Под, який падає, а потім залишити його так, бо інцидент вичерпано. Це міняє видимий збій на невидимий регрес безпеки. Якщо вам доводиться використовувати Unconfined під час діагностики, поводьтеся з ним як з тимчасовим feature flag, що має власника, крок видалення та профіль-заміну. Краща альтернатива — профіль у режимі скарг у тестовому шляху або вузьке правило дозволу на основі зафіксованої відмови.
Інший антипатерн — припускати, що Под DaemonSet, який працює на кожній ноді, означає, що профіль завантажено на кожній ноді. DaemonSet доводить, що контейнер було заплановано; він не доводить, що команда парсера успішно виконалася, AppArmor було ввімкнено або ім’я профілю досягло ядра. Хороше розгортання перевіряє код виходу команди, фіксує логи та перевіряє aa-status на хості. Ця перевірка — різниця між поширенням файлу та примусовим застосуванням політики.
Тонший антипатерн — копіювання профілю, орієнтованого на заборони, з одного образу в інший. Правила заборони мають сенс лише всередині повної моделі дозволів. Шлях, підозрілий для одного робочого навантаження, може бути нормальним для іншого, а широкий дозвіл file, може приховати той факт, що список заборон неповний. Використовуйте скопійовані профілі як стартові шаблони, а не як доказ. Доказ — це специфічний для робочого навантаження тест, що випробовує старт, усталену поведінку, перевірки працездатності та завершення.
| Патерн або антипатерн | Коли застосовувати | Чому працює або не вдається | Міркування щодо масштабування |
|---|---|---|---|
| Версіонувати профілі разом з конфігурацією ноди | Власні профілі Localhost захищають виробничі навантаження | Локальний профіль ноди стає придатним для огляду та повторюваним | Поєднуйте з перевіркою розгортання на кожній придатній для планування ноді |
Використовувати RuntimeDefault як базовий рівень | Робочі навантаження ще не профільовано | Це дає негайне загальне обмеження з низьким ризиком зламу | Зробіть це типовою політикою, потім затягуйте навантаження високого ризику |
Перевіряти з /proc та aa-status | Вам потрібен доказ, що примус активний | Це перевіряє позначку виконання та стан профілю на ноді, а не лише YAML | Автоматизуйте ці перевірки в димових тестах або аудитах безпеки |
Залишати Unconfined після налагодження | Профіль блокує старт, а тиск високий | Це знімає засіб контролю безпеки, водночас усуваючи симптом | Вимагайте відстеження видалення та явного профілю-заміни |
| Довіряти лише наявності DaemonSet | Профілі поширює привілейований помічник | Помічник може працювати, навіть коли розбір чи завантаження не вдалися | Перевіряйте коди виходу парсера та видимий хостом стан профілю |
| Копіювати профілі без тестів робочого навантаження | Команди хочуть швидкої стандартизації | Політика може кодувати поведінку неправильного застосунку | Шаблонізуйте лише структуру; тестуйте конкретний шлях навантаження |
Патерни та антипатерни корисні лише тоді, коли вони породжують конкретне рішення, за яким ви справді можете діяти. Для AppArmor це рішення зазвичай стосується впевненості в розгортанні: чи маєте ви профіль, що дійсно відповідає робочому навантаженню, чи мають його всі придатні ноди, і чи можете ви довести, що запущений процес його справді використовує? Якщо хоча б однієї з цих відповідей бракує, наступна дія — це не чергове гучне гасло про політику безпеки. Це конкретна, перевірна перевірка проти стану ноди, Пода чи логів.
Рамка прийняття рішень
Розділ «Рамка прийняття рішень»Використовуйте цю рамку, коли вам потрібно обрати між типовим профілем середовища виконання, власним локальним профілем та тимчасовим необмеженим порівнянням. Почніть з ризику робочого навантаження, а потім запитайте, яку поведінку ви можете описати та перевірити. Робоче навантаження високого ризику з добре відомою поведінкою заслуговує на власний профіль. Робоче навантаження низького ризику з невідомою поведінкою має почати з типового профілю середовища виконання й перейти до власного профілю після спостереження. Необмежений запуск — це діагностичний засіб, а не пункт призначення.
+-----------------------------------------------------------+| APPARMOR PROFILE DECISION FLOW |+-----------------------------------------------------------+| || Is AppArmor enabled on every eligible node? || | || +-- no --> fix node support before using profiles || | || +-- yes || | || v || Do you only need a broad baseline today? || | || +-- yes --> use RuntimeDefault || | || +-- no || | || v || Can you describe and test expected file/network use? || | || +-- yes --> create Localhost profile || | || +-- no --> observe in complain mode or test env || || Is the profile needed only for debugging? || | || +-- yes --> use Unconfined briefly, then remove || +-- no --> keep enforce mode and audit regularly || |+-----------------------------------------------------------+Рішення щодо профілю також залежить від того, де має статися збій. Якщо робоче навантаження явно запитує RuntimeDefault, а AppArmor вимкнено на ноді, Kubernetes може відхилити Под, а не тихо запустити його без профілю. Якщо поле пропущено, типовий профіль середовища виконання може застосуватися лише тоді, коли нода його підтримує. Ця різниця важлива в регульованих середовищах, бо «fail closed» (закритий збій) та «best effort» (за можливості) — не той самий операційний контракт.
| Вибір | Найкраще пасує | Головний компроміс | Команда перевірки |
|---|---|---|---|
RuntimeDefault | Швидкий базовий рівень для більшості контейнерів | Загальна політика, не специфічна для навантаження | kubectl exec <pod> -- cat /proc/1/attr/current |
Localhost | Відоме навантаження з конкретними обмеженнями | Потребує розгортання локального профілю на нодах | `sudo aa-status |
Unconfined | Коротке діагностичне порівняння | Знімає захист AppArmor | kubectl exec <pod> -- cat /proc/1/attr/current |
| Режим скарг | Розробка профілю до примусу | Логи можуть розростатися шумом під час тестування | sudo aa-status та journalctl -k |
| Мітка ноди плюс селектор | Профіль існує лише на частині нод | Зменшує гнучкість планувальника | kubectl get nodes --show-labels |
Який підхід ви обрали б для завдання, що читає ConfigMap, пише один вихідний файл у том emptyDir і ніколи не потребує мережі? Розумний шлях — власний профіль Localhost, що дозволяє очікувані файлові шляхи та забороняє мережу, але лише після того, як ви підтвердите, що образ не використовує DNS, завантаження пакетів чи телеметрію під час старту. Якщо ви ще не можете підтвердити цю поведінку, почніть у тестовому просторі імен з режимом скарг, а не відвантажуйте вгаданий профіль примусу.
Рамка прийняття рішень також має впливати на те, як ви пишете лабораторні свідчення. «Застосував профіль» слабше, ніж «застосував профіль, перевірив активну позначку, спробував заборонену операцію та знайшов відповідну відмову ядра». Сильніше твердження містить бажаний результат і шлях доказу. В екзаменаційній роботі цей шлях доказу допомагає вам не втрачати час, коли маніфест виглядає правильним, але kubelet відхилив контейнер, бо профіль був відсутній на ноді.
Чи знали ви?
Розділ «Чи знали ви?»- Підтримка AppArmor для профілів Kubernetes стабільна, починаючи з Kubernetes v1.31, а Kubernetes 1.35+ використовує структуроване поле
appArmorProfile, а не старішу бета-анотацію. - Профіль AppArmor має бути завантажений у ядро ноди, перш ніж kubelet зможе запустити контейнер з
type: Localhost; API Kubernetes не зберігає текст профілю замість вас. - Конфігурація
appArmorProfileна рівні контейнера має пріоритет над конфігурацією на рівні Пода, що важливо, коли основному контейнеру та sidecar потрібні різні обмеження. - Такі інструменти, як
aa-genprofіaa-logprof, можуть допомогти згенерувати або вдосконалити профілі зі спостережуваної поведінки застосунку, але результат усе одно потребує людського огляду перед примусом.
Типові помилки
Розділ «Типові помилки»| Помилка | Чому вона трапляється | Як її виправити |
|---|---|---|
| Завантаження профілю лише на одній ноді | Перший тестовий Под працює, тож про локальну залежність ноди забувають | Завантажте та перевірте профіль на кожній придатній ноді або обмежте планування мітками |
| Використання застарілих анотацій у маніфесті Kubernetes 1.35+ | Старіші приклади все ще показують форму API до v1.30 | Використовуйте securityContext.appArmorProfile, а анотації тримайте лише для розпізнавання при міграції |
| Зіставлення профілю з іменем образу замість імені контейнера | Приклади застарілих анотацій вбудовують ім’я в ключ, що провокує плутанину | Для старих маніфестів точно зіставляйте ім’я контейнера; для сучасних задавайте контекст безпеки контейнера напряму |
| Пропуск спостереження в режимі скарг чи тестовому середовищі | Правило заборони виглядає очевидним під час огляду, але блокує поведінку старту | Випробуйте старт, перевірки працездатності, усталену роботу та завершення перед переходом у режим примусу |
| Припущення, що розгортання DaemonSet дорівнює реєстрації в ядрі | Помічник поширення може працювати, навіть якщо apparmor_parser зазнав невдачі | Перевіряйте коди виходу парсера й підтверджуйте через aa-status або /sys/kernel/security/apparmor/profiles |
Сприйняття Unconfined як постійного виправлення | Він швидко усуває збій під тиском | Використовуйте його лише для короткої діагностики, потім замініть на RuntimeDefault чи перевірений власний профіль |
| Аудит лише маніфесту | YAML показує запитану конфігурацію, а не позначку, застосовану до процесу | Перевіряйте зсередини контейнера через /proc/1/attr/current та з ноди інструментарієм AppArmor |
Тест
Розділ «Тест»1. Ваша команда спроєктувала профіль `k8s-deny-write`, завантажила його на одну робочу ноду та застосувала Под з `type: Localhost`. Под працює під час першого тесту, але падає після того, як спорожнення (drain) перемістило його на іншу ноду. Що слід перевірити насамперед і чому?
Перевірте, чи завантажено профіль на новій ноді, через sudo aa-status | grep k8s-deny-write або прочитавши /sys/kernel/security/apparmor/profiles. Профіль Localhost — це локальний актив ядра ноди, а не об’єкт, що зберігається в API Kubernetes. Планувальник не вибирає ноди на основі завантажених профілів AppArmor, якщо ви не додасте власні мітки та обмеження. Якщо профіль відсутній, завантажте його на кожну придатну ноду або обмежте Под нодами, де профіль навмисно присутній.
2. Маніфест Пода для Kubernetes 1.35 використовує `container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-deny-network`. Профіль працює на старішому кластері, але ваш рецензент просить сучасний API. Як ви перепишете маніфест?
Перемістіть посилання на профіль у поле securityContext.appArmorProfile контейнера. Використайте type: Localhost та localhostProfile: k8s-deny-network на контейнері з іменем app. Стара анотація була способом виразити той самий намір до v1.30, але сучасна документація Kubernetes спрямовує користувачів до структурованого поля. Цей перепис також робить конфігурацію легшою для перевірки політикою допуску та інструментами, що враховують схему.
3. Контейнер запускається під новим профілем і одразу завершується зі звичайною помилкою застосунку про неможливість записати згенерований файл. Дозволи Unix виглядають нормальними. Які свідчення підтвердили б AppArmor як причину?
Шукайте відмову AppArmor у sudo dmesg | grep -i apparmor або sudo journalctl -k | grep -i apparmor. Запис у журналі має іменувати активний профіль, операцію, шлях та відхилену маску. Також перевірте позначку виконання через kubectl exec <pod> -- cat /proc/1/attr/current, якщо контейнер залишається запущеним достатньо довго. Якщо шлях відмови збігається зі згенерованим файлом, вирішіть, чи дозволити саме цей шлях, чи змінити поведінку застосунку.
4. Вас просять покращити обмеження для багатьох наявних робочих навантажень цього тижня, але у вас ще немає специфічних для навантаження профілів поведінки. Який варіант AppArmor дає негайну цінність із найнижчим ризиком зламу?
Використовуйте RuntimeDefault як негайний базовий рівень. Він застосовує типовий профіль AppArmor середовища виконання контейнерів, який зазвичай блокує поширені небезпечні операції, водночас дозволяючи звичайну поведінку контейнера. Власні профілі Localhost можуть забезпечити сильніше специфічне для навантаження обмеження пізніше, після спостереження та тестування. Не використовуйте Unconfined як широкий базовий рівень, бо він навмисно знімає примус AppArmor.
5. DaemonSet, що поширює профілі, працює на кожній робочій ноді, але одна нода все одно відхиляє Поди, які запитують `k8s-deny-sensitive`. Що це каже вам про розгортання?
Це каже вам, що планування DaemonSet — не те саме, що успішна реєстрація профілю. Контейнер-помічник міг запуститися, тоді як його команда парсера зазнала невдачі, AppArmor може бути вимкнено на цій ноді, або файл профілю може мати синтаксичну проблему. Перевірте логи Пода DaemonSet та статус виходу apparmor_parser, а потім підтвердьте профіль з хоста через aa-status. Виправлення полягає в тому, щоб зробити завантаження профілю спостережуваним, а не просто перезапустити DaemonSet.
6. Огляд безпеки знаходить `type: Unconfined` на виробничому Поді, який раніше мав збої старту під власним профілем. Який безпечніший шлях усунення?
Спершу з’ясуйте, чи додано Unconfined як тимчасовий діагностичний крок, чи як недокументований постійний обхідний шлях. Замініть його на RuntimeDefault, якщо вам потрібне негайне базове обмеження, а власний профіль не готовий. Потім відтворіть збій старту в тестовому середовищі, дослідіть логи AppArmor та додайте вузькі правила дозволу лише для поведінки, яка застосунку законно потрібна. Мета — зняти необмежений стан, не вгадуючи широкий власний профіль.
7. Вам потрібно провести аудит, чи Под насправді працює під призначеним профілем після оновлення маніфесту. Чому читання лише YAML недостатнє, і що слід запустити?
YAML показує запитану конфігурацію, але не доводить, що kubelet допустив Под із цим профілем, чи що процес несе очікувану позначку ядра. Запустіть kubectl exec <pod> -- cat /proc/1/attr/current, щоб перевірити активну позначку всередині контейнера. Також перевірте ноду через sudo aa-status | grep <profile>, коли тип профілю — Localhost. Повний аудит порівнює намір маніфесту, стан профілю на ноді та стан процесу під час виконання.
Практична вправа
Розділ «Практична вправа»Передумова: Ця лабораторія потребує робочого вузла Linux з увімкненим AppArmor (типовий Ubuntu для CKS/killercoda). Якщо cat /sys/module/apparmor/parameters/enabled не повертає Y, виконайте кроки з профілем на ноді на підтримуваному хості, перш ніж застосовувати Поди.
У цій вправі ви створите власний профіль AppArmor, що забороняє мережевий доступ, завантажите його на ноду, застосуєте його до Пода за допомогою поля контексту безпеки Kubernetes 1.35+ та перевірите як заплановану відмову, так і контрольний випадок. Оригінальну послідовність команд лабораторії збережено за духом, але маніфест використовує appArmorProfile замість застарілої анотації. Запускайте команди ноди на робочому вузлі, що хоститиме Под, або використовуйте однонодовий лабораторний кластер, де нода площини управління також запускає робочі навантаження.
- Підтвердьте, що AppArmor увімкнено на ноді та що
aa-statusможе звітувати про завантажені профілі. - Створіть і завантажте профіль
k8s-deny-networkза допомогоюapparmor_parser. - Запустіть Под, що використовує
type: LocalhostтаlocalhostProfile: k8s-deny-network. - Перевірте активний профіль через
/proc/1/attr/currentусередині контейнера. - Доведіть, що мережевий доступ зазнає невдачі в обмеженому Поді та успішно виконується в необмеженому Поді для порівняння.
- Приберіть Поди та видаліть профіль, якщо це тимчасова лабораторна нода.
# Step 1: Check AppArmor is enabled (run on node)cat /sys/module/apparmor/parameters/enabled# Should output: Y
# Step 2: Create the profilesudo tee /etc/apparmor.d/k8s-deny-network << 'EOF'#include <tunables/global>
profile k8s-deny-network flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base>
# Allow file operations file,
# Deny network access deny network,}EOF
# Step 3: Load the profilesudo apparmor_parser -r /etc/apparmor.d/k8s-deny-network
# Step 4: Verify it's loadedsudo aa-status | grep k8s-deny-network
# Step 5: Create pod with the profile using Kubernetes 1.35+ appArmorProfilecat <<EOF | kubectl apply -f -apiVersion: v1kind: Podmetadata: name: no-network-podspec: containers: - name: app image: curlimages/curl command: ["sleep", "3600"] securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-deny-networkEOF
# Step 6: Wait for podkubectl wait --for=condition=Ready pod/no-network-pod --timeout=60s
# Step 7: Verify the profile labelkubectl exec no-network-pod -- cat /proc/1/attr/current
# Step 8: Test network is blockedkubectl exec no-network-pod -- curl -s https://kubernetes.io --connect-timeout 5# Should fail due to AppArmor denying network
# Step 9: Create pod without restriction for comparisonkubectl run network-allowed --image=curlimages/curl --rm -i --restart=Never -- \ curl -s https://kubernetes.io -o /dev/null -w "%{http_code}"# Should succeed (200)
# Cleanupkubectl delete pod no-network-pod --grace-period=0sudo apparmor_parser -R /etc/apparmor.d/k8s-deny-networkНотатки до розв'язання завдань 1-3
Перевірка ноди має повернути Y з /sys/module/apparmor/parameters/enabled, а sudo aa-status має повідомити, що модуль AppArmor завантажено. Файл профілю належить під /etc/apparmor.d/, а sudo apparmor_parser -r /etc/apparmor.d/k8s-deny-network має успішно завершитися. Якщо aa-status | grep k8s-deny-network нічого не виводить, виправте стан ноди, перш ніж налагоджувати Kubernetes. Под не може використати профіль Localhost, який вибрана нода не завантажила.
Нотатки до розв'язання завдань 4-5
Вивід /proc/1/attr/current має містити k8s-deny-network та маркер режиму примусу. Команда curl усередині no-network-pod має зазнати невдачі, бо профіль забороняє мережеві операції, перш ніж процес зможе завершити з’єднання. Под для порівняння має повернути код HTTP-статусу, бо він не використовує профіль заборони мережі. Якщо обидва Поди падають, налагоджуйте DNS чи загальний вихідний трафік кластера, перш ніж звинувачувати AppArmor.
Нотатки до розв'язання завдання 6
Видалення Пода прибирає робоче навантаження, але не прибирає профіль хоста. Використовуйте sudo apparmor_parser -R /etc/apparmor.d/k8s-deny-network лише тоді, коли профіль було створено для цієї лабораторії й жоден інший Под від нього не залежить. У спільному навчальному кластері перевірте посилання перед видаленням. У реальному кластері видалення профілю має слідувати тому самому процесу релізу, що й розгортання профілю.
Критерії успіху навмисно ґрунтуються на свідченнях. Ви маєте змогти показати, що нода має профіль, контейнер працює з цим профілем, заборонена операція зазнає невдачі, контрольна операція успішно виконується поза профілем, а крок очищення не залишає тимчасового Пода. Якщо хоч одне з цих тверджень не підкріплене, продовжуйте налагодження, доки спостережуваний стан і запланована політика не збіжаться.
Перевірка засвоєного
Розділ «Перевірка засвоєного»Профілі AppArmor — це локальні активи ядра ноди: Kubernetes 1.35+ посилається на них через
securityContext.appArmorProfile, але кожна робоча нода, що може запускати Под, має завантажити профіль, перш ніж kubelet зможе примуситиtype: Localhost.
Перш ніж рухатися далі, поясніть, чому Под може пройти допуск, але впасти на спорожненій ноді, і які три перевірки доводять примус (стан профілю на ноді, події Пода та /proc/1/attr/current). Ґрунтовна відповідь називає обмеження Pod Security Admission на Unconfined, відрізняє відсутні профілі Localhost на kubelet від відхилення під час допуску та наводить принаймні один шлях до журналу відмов ядра.
Джерела
Розділ «Джерела»- https://kubernetes.io/docs/tutorials/security/apparmor/
- https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
- https://kubernetes.io/docs/concepts/security/linux-kernel-security-constraints/
- https://kubernetes.io/docs/concepts/security/pod-security-standards/
- https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#apparmorprofile-v1-core
- https://kubernetes.io/docs/reference/kubectl/generated/kubectl_exec/
- https://apparmor-documentation-c38b15.gitlab.io/documentation/getting-started/profiles-basics/
- https://apparmor-documentation-c38b15.gitlab.io/documentation/in-depth/profiles/core-policy-reference/
- https://apparmor-documentation-c38b15.gitlab.io/documentation/getting-started/complain-mode/
- https://manpages.ubuntu.com/manpages/noble/en/man8/aa-status.8.html
- https://manpages.ubuntu.com/manpages/noble/en/man8/apparmor_parser.8.html
- https://docs.docker.com/engine/security/apparmor/
Наступний модуль
Розділ «Наступний модуль»Модуль 3.2: Профілі Seccomp — фільтрація системних викликів для контейнерів.