Перейти до вмісту

Модуль 4.4: SecurityContext

Hands-On Lab Available
K8s Cluster advanced 40 min
Launch Lab ↗

Opens in Killercoda in a new tab

Складність: [СЕРЕДНЯ] — важлива для безпеки, багато налаштувань.

Час на проходження: 40-50 хвилин.

Передумови: базові поняття користувачів і груп у Linux, Модуль 1.1 (Поди).


Після завершення цього модуля ви зможете:

  • Впроваджувати SecurityContext на рівні Пода й на рівні контейнера для runAsUser, runAsGroup, fsGroup та runAsNonRoot.
  • Діагностувати невдалі запуски Подів та помилки прав доступу, спричинені runAsNonRoot, readOnlyRootFilesystem і власністю на томи.
  • Оцінювати налаштування Linux-можливостей (capabilities) та підвищення привілеїв, щоб обирати доступ контейнера за принципом найменших привілеїв.
  • Проєктувати придатний для CKAD маніфест безпечного Пода, який зберігає записувані шляхи через томи emptyDir і відповідає очікуванням Pod Security у Kubernetes 1.35+.

Чому цей модуль важливий

Розділ «Чому цей модуль важливий»

Гіпотетичний сценарій: команда відвантажує невеликий HTTP-сервіс у спільний кластер Kubernetes, і образ ідеально працює на ноутбуці, тому що запускається від root і може писати будь-куди у файлову систему контейнера. У кластері той самий деплоймент потрапляє до простору імен з обмежувальними налаштуваннями Pod Security, контейнер відмовляється стартувати, і єдиною видимою підказкою є коротка подія про запуск від root. Програма не зламана, але маніфест не зміг описати той контракт безпеки, якого очікує кластер.

SecurityContext — це та частина специфікації Пода, де ви перетворюєте припущення безпеки Linux на конфігурацію Kubernetes. Він відповідає на практичні запитання, які виникають під час розгортань і усунення несправностей на CKAD: під яким UID працює процес, яка група володіє файлами змонтованого тома, чи є коренева файлова система записуваною, чи може процес отримати більше привілеїв і які вузько окреслені Linux-можливості лишаються доступними. Ці налаштування не є декоративними важелями посилення безпеки. Вони безпосередньо впливають на те, чи запуститься контейнер, чи зможе він відкрити файли і чи відповідає він політиці допуску кластера.

Екзамен CKAD зазвичай тримає завдання з SecurityContext невеликими, але перевіряє ту саму ментальну модель, яка потрібна продакшн-кластерам. Вас можуть попросити зробити так, щоб Под працював від не-root UID, виправити проблему з правами на том, додати записуваний emptyDir для програми з кореневою файловою системою лише для читання, або переглянути фактичні налаштування запущеного Пода. Кожне завдання швидке лише тоді, коли ви вже знаєте, які налаштування належать рівню Пода, які — рівню контейнера, і як Kubernetes розв’язує конфлікти.

Сприймайте SecurityContext як контроль доступу до будівлі. Політика будівлі може визначати типовий бейдж, який отримує кожен орендар, але якась кімната все одно може потребувати суворішого обмеження чи іншого ключа. SecurityContext на рівні Пода надає типові значення для контейнерів цього Пода, тоді як SecurityContext на рівні контейнера може перевизначити окремі налаштування процесу й привілеїв для одного контейнера. Змонтовані томи — це спільні кімнати, тож власність на групу та записувані каталоги слід планувати з огляду на кожен контейнер.

Цей модуль навчає SecurityContext як інструменту налагодження не менше, ніж інструменту безпеки. Ви почнете з ієрархії, а потім перейдете до ідентичності, власності на томи, записуваних шляхів, Linux-можливостей і команд перевірки. Зрештою маніфест з runAsNonRoot, fsGroup, readOnlyRootFilesystem і відкинутими можливостями має читатися як чіткий операційний контракт, а не купа фрагментів YAML.

У Kubernetes є два пов’язані розташування SecurityContext, і плутати їх — найшвидший спосіб написати маніфест, який виглядає безпечним, але поводиться інакше, ніж очікувалося. spec.securityContext на рівні Пода задає типові значення й поведінку для всього Пода, особливо типову ідентичність та обробку груп томів. containers[].securityContext на рівні контейнера керує налаштуваннями, що належать процесу одного контейнера, такими як підвищення привілеїв, можливості, коренева файлова система лише для читання та специфічне для контейнера перевизначення runAsUser.

Цей поділ існує, тому що Под може містити більше одного контейнера. Головний контейнер програми може працювати від UID 1000, тоді як образ-сайдкар може потребувати UID 2000, бо його шляхи у файловій системі будувалися з іншою власністю. Под усе одно може визначити fsGroup для спільних томів, бо том має бути придатним до використання обома контейнерами. Kubernetes дає вам модель типових значень замість того, щоб змушувати кожен контейнер повторювати ті самі поля ідентичності.

Правило пріоритету просте: коли те саме підтримуване поле існує на обох рівнях, для цього контейнера перемагає значення рівня контейнера. Це не означає, що значення рівня Пода зникає. Інші контейнери все одно успадковують його, а налаштування, що існують лише на рівні Пода, як-от fsGroup, лишаються в межах Пода. Добрий маніфест використовує налаштування рівня Пода для спільних типових значень, а налаштування рівня контейнера — коли якийсь образ має законну причину відрізнятися.

apiVersion: v1
kind: Pod
metadata:
name: security-demo
spec:
securityContext: # Pod-level (applies to all containers)
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: nginx
securityContext: # Container-level (overrides pod-level)
runAsUser: 2000
allowPrivilegeEscalation: false

У цьому прикладі контейнер app працює від UID 2000, бо runAsUser на рівні контейнера перевизначає типове значення рівня Пода. Поведінка власності на групу все одно походить з контексту рівня Пода: runAsGroup: 3000 надає первинну групу, коли вона не перевизначена, а fsGroup: 2000 використовується для змонтованих томів, що підтримують керування власністю. Якщо ви додасте ще один контейнер без runAsUser на рівні контейнера, цей другий контейнер успадкує UID 1000.

Зробіть паузу і спрогнозуйте: якщо Под задає runAsNonRoot: true на рівні Пода, а один контейнер задає runAsUser: 0, що, на вашу думку, зробить Kubernetes, коли спробує запустити цей контейнер? Важлива деталь полягає в тому, що runAsNonRoot — це не запит на зміну UID. Це запобіжник, який каже, що контейнер не повинен працювати від UID 0, тож явне перевизначення на root конфліктує із запобіжником, і запуск має завершитися невдачею.

Ієрархію легше запам’ятати, якщо відокремити типові значення від винятків. Розмістіть типову ідентичність на рівні Пода, коли кожен контейнер може її розділяти, розмістіть спільну власність на томи на рівні Пода, бо томи є ресурсами Пода, а налаштування, чутливі до привілеїв, розмістіть на рівні контейнера, бо привілеї прив’язані до окремих процесів. Коли сайдкар потребує іншого UID, зробіть перевизначення явним і тримайте причину видимою в маніфесті.

┌─────────────────────────────────────────────────────────────┐
│ SecurityContext Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ │
│ Pod Security Context │
│ ┌─────────────────────────────────────────────────┐ │
│ │ runAsUser: 1000 │ │
│ │ runAsGroup: 3000 │ │
│ │ fsGroup: 2000 (for volumes) │ │
│ │ │ │
│ │ Container A Container B │ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ (inherits pod) │ │ runAsUser: 2000│ │ │
│ │ │ runAsUser:1000 │ │ (overrides pod)│ │ │
│ │ │ │ │ │ │ │
│ │ │ capabilities: │ │ readOnly: true │ │ │
│ │ │ drop: [ALL] │ │ │ │ │
│ │ └────────────────┘ └────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Діаграма показує операційне правило, яке ви використовуватимете під час усунення несправностей. Спершу перегляньте типові значення рівня Пода, потім перегляньте кожен контейнер на наявність перевизначень, і нарешті пам’ятайте, що деякі поля ніколи не стають рішеннями рівня контейнера. Цей порядок запобігає поширеній помилці: багаторазовій зміні значення рівня Пода, тоді як фактична поведінка походить від перевизначення в контейнері далі вниз у специфікації.

Ідентичність, групи та власність на томи

Розділ «Ідентичність, групи та власність на томи»

Процеси Linux працюють з числовим ідентифікатором користувача та групи, і SecurityContext у Kubernetes дозволяє задати ці числа без перебудови образу. Образ усе ще може оголошувати типового користувача, але runAsUser у специфікації Пода чи контейнера є чіткішим з погляду кластера. На екзамені використання числового UID зазвичай безпечніше, ніж покладання на ім’я користувача, бо імена потребують записів у /etc/passwd усередині образу, тоді як числові ідентифікатори ядро розуміє напряму.

securityContext:
runAsUser: 1000 # UID to run as
runAsGroup: 3000 # GID for the process
fsGroup: 2000 # GID for mounted volumes

runAsUser змінює UID головного процесу в контейнері, а runAsGroup змінює його первинну групу. fsGroup відрізняється: це налаштування рівня Пода, яке надає контейнерам додаткову групу для підтримуваних монтувань томів і може коригувати власність на групу для вмісту тома. Ця відмінність важлива, бо файли програми, запечені в образ, є частиною кореневої файлової системи контейнера, тоді як змонтовані томи приєднує Kubernetes під час виконання.

Якщо контейнер може запуститися від UID 1000, але не може писати до /data, справжньою проблемою може бути не UID. Каталог може бути змонтований з тома, власність на групу якого не збігається з групами процесу. У такому разі fsGroup часто виправляє доступ до тома, зберігаючи при цьому не-root процес. Саме тому налагодження прав на томи починається з id та ls -la, а не з випадкових змін кожного поля SecurityContext.

securityContext:
runAsNonRoot: true # Fail if image tries to run as root

runAsNonRoot: true часто розуміють хибно, бо назва звучить як інструкція. Краще читати її як твердження: «цей контейнер не повинен працювати від root». Якщо Kubernetes може визначити, що образ запустився б від UID 0, або якщо ви явно задаєте runAsUser: 0, контейнер відхиляється. Якщо образ використовує іменованого користувача, а Kubernetes не може перевірити числовий UID, поведінка може залежати від деталей середовища виконання, тож явний числовий runAsUser лишається чіткішим вибором.

Перш ніж це запускати, який вивід ви очікуєте від id, якщо Под задає runAsUser: 1000, runAsGroup: 3000 та fsGroup: 2000? Правильний прогноз має згадати UID 1000, первинний GID 3000 і додаткову групу, що включає 2000. Цей прогноз дає вам конкретну ціль, коли ви згодом оглядаєте Под, і не дозволяє сприймати кожну помилку прав доступу як загадку.

apiVersion: v1
kind: Pod
metadata:
name: volume-perms
spec:
securityContext:
runAsUser: 1000
fsGroup: 2000 # Volume files owned by this group
containers:
- name: app
image: busybox
command: ['sh', '-c', 'ls -la /data && sleep 3600']
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}

Цей Под volume-perms навмисно малий, бо ізолює поведінку. Процес працює від UID 1000, змонтований emptyDir з’являється на /data, а fsGroup: 2000 надає контейнеру груповий зв’язок з цим томом. У реальних системах зберігання зміни власності можуть мати поведінку, специфічну для продуктивності та драйвера, але рішення на рівні CKAD лишається тим самим: використовуйте fsGroup для доступу до тома Пода, а не для файлів, які вже є всередині образу.

Деякі образи дають збій після зміни UID, бо їхні записувані каталоги належали root під час збирання образу. Цей збій — не непослідовність Kubernetes; це права Linux роблять саме те, що мають робити. Якщо образ очікує писати під /var/cache/app, вам або потрібно, щоб образ підтримував не-root UID, або потрібно змонтувати записуваний том на тому шляху, який має лишатися змінним.

Найнадійніша послідовність усунення несправностей є вузькою і повторюваною. Спершу перегляньте події, щоб відрізнити збої допуску від збоїв середовища виконання, далі перегляньте ієрархію SecurityContext, а потім запустіть id та переліки каталогів, якщо контейнер стартує. Якщо контейнер не стартує взагалі, порівняйте типового користувача образу з вашими налаштуваннями runAsNonRoot і runAsUser. Якщо він стартує, але не може писати, перегляньте цільовий шлях і вирішіть, що буде правильним виправленням: fsGroup, emptyDir чи перебудова образу.

Файлові системи, записувані шляхи та коренева система лише для читання

Розділ «Файлові системи, записувані шляхи та коренева система лише для читання»

readOnlyRootFilesystem: true робить кореневу файлову систему контейнера незмінною з погляду процесу. Це цінно, бо зловмисник або процес із дефектом не може переписати двійкові файли програми, файли конфігурації, стан менеджера пакетів чи скрипти запуску в шарі образу. Компроміс є негайним: багато звичайних програм пишуть тимчасові файли, сокети, PID-файли, кеші чи логи під шляхами, які лежать у кореневій файловій системі, доки ви не надасте їм явних записуваних монтувань.

securityContext:
readOnlyRootFilesystem: true # Container can't write to filesystem

Коренева система лише для читання не означає, що контейнер не може писати ніде. Це означає, що записи мають іти до змонтованих томів чи інших записуваних файлових систем, які ви навмисно надаєте. Для короткочасного робочого простору emptyDir є звичайною відповіддю на CKAD, бо його легко оголосити, він прив’язаний до життєвого циклу Пода і не потребує окремого об’єкта зберігання. Для довговічних даних програми ви б обрали натомість персистентний том, але це рішення проєктування зберігання, а не швидкий хід через SecurityContext.

Наведений нижче безпечний приклад у стилі nginx зберігає форму оригінального модуля, бо демонструє точне поєднання, яке учні мають розпізнавати. Контейнер має кореневу файлову систему лише для читання, відкидає можливості й вимикає підвищення привілеїв, але все одно монтує записувані томи emptyDir на /tmp, /var/cache/nginx та /var/run. Контроль безпеки лишається непорушним, бо записуваними є лише відомі шляхи виконання.

apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}

Офіційний образ nginx за замовчуванням слухає порт 80. Порти нижче 1024 традиційно потребували можливості NET_BIND_SERVICE, проте цей приклад відкидає всі можливості й усе одно стартує на багатьох тренувальних кластерах, бо сучасні середовища виконання контейнерів можуть задавати net.ipv4.ip_unprivileged_port_start=0, що дозволяє непривілейованим процесам прив’язуватися до низьких портів. Завдання CKAD, які явно вимагають прив’язки до порту нижче 1024, усе одно очікують, що ви додасте NET_BIND_SERVICE, а не покладатиметеся на цю поведінку sysctl середовища виконання.

Зупиніться й подумайте: якщо програма тепер зазнає невдачі з permission denied під час запису /var/log/nginx/access.log, чи варто вимкнути readOnlyRootFilesystem, змінити UID, чи додати записуване монтування для шляху логів? Найкраща відповідь залежить від того, чи справді програмі потрібно туди писати. Якщо запис є очікуваною поведінкою виконання, додайте конкретне монтування тома або переналаштуйте програму на логування до stdout; не прибирайте ширший захист кореневої системи лише для читання заради того, щоб приховати один відсутній записуваний шлях.

Операційна звичка — робити записувані шляхи буденними й явними. Рецензент має змогти переглянути маніфест і побачити, що /tmp записуваний, бо це робочий простір, /var/run записуваний, бо там може створюватися сокет чи PID-файл, а дані програми обробляються правильним типом зберігання. Коли ці шляхи не явні, програма може пройти тести розробки й дати збій лише після того, як політика кластера примусово застосує кореневу систему лише для читання.

Тут також є урок про пакування. Добре зібраний образ контейнера може працювати від не-root UID, писати тимчасові дані лише до задокументованих шляхів і надсилати логи до stdout чи stderr. SecurityContext може примушувати ці очікування, але не може чарівним чином полагодити образ, який припускає root-доступ до кожного каталогу. Для завдань CKAD ви зазвичай виправляєте маніфест; для продакшн-роботи ви часто виправляєте і образ, і маніфест, щоб контракт був чітким на кожному рівні.

Можливості та межі привілеїв

Розділ «Можливості та межі привілеїв»

Linux-можливості (capabilities) розщеплюють деякі повноваження root на іменовані привілеї. Замість того, щоб робити контейнер повністю привілейованим, ви можете надати вузьку можливість, як-от NET_BIND_SERVICE, коли процес має прив’язатися до низького порту, або NET_ADMIN, коли він має налаштовувати мережеві параметри. Безпечніше типове рішення — відкинути всі можливості й додати назад лише ті, які робоче навантаження доказово потребує.

securityContext:
capabilities:
add:
- NET_ADMIN # Network configuration
- SYS_TIME # System clock
drop:
- ALL # Remove all capabilities first

Можливості потужні, бо лежать нижче Kubernetes у ядрі Linux. Помилка з можливістю може мати значення навіть тоді, коли процес не працює від UID 0, бо можливості надають вибрані привілейовані операції. Саме тому «не-root» і «найменші привілеї» пов’язані, але не тотожні. Не-root процес із надмірними можливостями все одно може робити речі, яких звичайний процес програми не повинен мати змоги робити ніколи.

CapabilityПризначення
NET_ADMINНалаштування мережі
NET_BIND_SERVICEПрив’язка до портів < 1024
SYS_TIMEЗміна системного годинника
SYS_PTRACEНалагодження інших процесів
CHOWNЗміна власності на файли

Таблиця перелічує поширені можливості, які ви можете побачити в прикладах, а не рекомендацію використовувати їх безтурботно. NET_BIND_SERVICE значно вужча за NET_ADMIN, а SYS_TIME рідко є доречною для контейнера програми. Коли завдання просить вас прив’язатися до порту нижче 1024, додайте NET_BIND_SERVICE, навіть якщо ваш локальний кластер випадково дозволяє непривілейовані низькі порти через ip_unprivileged_port_start. SYS_PTRACE може бути корисною для інструментів налагодження, але є ризикованою у звичайних робочих навантаженнях. CHOWN може приховувати проблеми з правами образу, якщо ви використовуєте її як милицю замість виправлення власності під час збирання.

securityContext:
allowPrivilegeEscalation: false # Prevent gaining more privileges

allowPrivilegeEscalation: false не дає процесу отримати більше привілеїв, ніж його батьківський процес, зокрема через setuid-двійкові файли. У Kubernetes це налаштування тісно пов’язане з поведінкою Linux no_new_privs. Це сильне типове значення для контейнерів програм, бо звичайні сервіси не повинні потребувати перетворення запуску з нижчими привілеями на стан виконання з вищими привілеями.

securityContext:
privileged: true # Full host access (DANGEROUS - rarely needed)

privileged: true — протилежний кінець спектра. Він дає контейнеру широкий доступ до хоста, зокрема можливості та доступ до пристроїв, що обходять звичайну модель ізоляції. Деякі системні агенти рівня вузла потребують такого доступу, але більшість Подів програм CKAD — ні. Якщо завдання просить можливість, додайте можливість; якщо воно просить виконання від не-root, привілейований режим майже напевно є хибним напрямком.

Який підхід ви б тут обрали і чому: надати privileged: true, щоб контейнер міг коригувати правила iptables, чи відкинути всі можливості й додати лише NET_ADMIN? Другий підхід є відповіддю з найменшими привілеями, бо надає конкретну операцію ядра, не даючи контейнеру кожну доступну привілейовану операцію. Вам усе одно варто запитати, чи має програма взагалі змінювати iptables, але вибір на рівні маніфесту є чітким.

securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE

Патерн «відкинути все, додати конкретне» легко обстоювати, бо він робить виняток видимим. Рецензентам не треба гадати, які типові можливості надало середовище виконання, а майбутні супровідники можуть точно побачити, чому можливість було додано. У роботі на CKAD цей патерн також запобігає випадковому надмірному наданню, коли запитання просить одну іменовану можливість і нічого більше.

Можливості взаємодіють з іншими засобами контролю, а не замінюють їх. Ви можете працювати від не-root UID, заборонити підвищення привілеїв, використати кореневу файлову систему лише для читання й усе одно додати одну конкретну можливість за потреби. Цей багаторівневий підхід практичний, бо кожне поле обробляє інший режим збою: ідентичність керує власником процесу, підвищення привілеїв керує змінами під час виконання, можливості керують повноваженнями ядра, а налаштування файлової системи керують записуваними поверхнями.

Робочий процес перевірки та налагодження

Розділ «Робочий процес перевірки та налагодження»

Налагодження SecurityContext має ґрунтуватися на доказах. Почніть із налаштованих полів об’єкта API, а потім перегляньте докази виконання всередині контейнера, якщо він стартує. Невдалий запуск може бути спричинений політикою допуску, конфліктами користувача образу чи перевіркою виконання, як-от runAsNonRoot. Запущений контейнер із помилками прав доступу потребує іншого шляху: перегляньте UID, групи, власність на монтування й точний каталог, який дав збій.

Terminal window
# Check what user process runs as
kubectl exec my-pod -- id
# uid=1000 gid=3000 groups=2000
# Check file ownership in volume
kubectl exec my-pod -- ls -la /data
# drwxrwsrwx 2 root 2000 ...
# Check capabilities
kubectl exec my-pod -- cat /proc/1/status | grep Cap

Команда id повідомляє ідентичність процесу, яку Linux фактично використовує. Перелік каталогу повідомляє, чи є цільовий шлях записуваним для цієї ідентичності чи однієї з її груп. Статус можливостей у /proc/1/status є нижчого рівня й закодований, але він усе одно корисний, коли вам потрібен доказ зміни можливостей. Заради швидкості на CKAD перші дві команди зазвичай розв’язують більшість питань прав доступу SecurityContext.

Не пропускайте kubectl describe pod, коли контейнер дає збій ще до того, як ви можете виконати exec до нього. Події часто кажуть, чи Kubernetes відмовив контейнеру, бо той працював би від root, чи монтування тома зазнало невдачі, чи контейнер увійшов у цикл збоїв після старту процесу. Ця різниця важлива, бо збій допуску SecurityContext виправляється в маніфесті, тоді як збій програми може потребувати записуваних шляхів чи змін прав на рівні образу.

# Pod-level
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
runAsNonRoot: true
# Container-level
containers:
- name: app
securityContext:
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]

Цей короткий довідник навмисно розділено за рівнями. Якщо ви швидко редагуєте під час екзамену, він нагадує не ставити fsGroup під окремий контейнер і не ховати налаштування лише для контейнера на хибному рівні. Kubernetes відхиляє деякі неправильно розміщені поля під час валідації, але інші помилки тонші, бо чинне значення на хибному рівні змінює хибну область дії.

Коли ви оглядаєте живий Под, віддавайте перевагу прямим запитанням над широкими дампами. kubectl get pod secure-pod -o yaml корисна, але може поховати потрібні рядки в довгому документі. Сфокусований JSONPath може бути швидшим, коли ви знаєте поле, тоді як describe краще підходить для подій і стану високого рівня. Ключ — рухатися від симптому до доказу, а не від симптому до здогаду.

Terminal window
kubectl get pod secure-pod -o jsonpath='{.spec.securityContext}{"\n"}'
kubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext}{"\n"}'
kubectl describe pod secure-pod

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

Розбір прикладу: проєктування безпечного Пода

Розділ «Розбір прикладу: проєктування безпечного Пода»

Сценарій вправи: ви отримуєте завдання CKAD, яке просить Под на ім’я secure-pod, що запускає nginx, з не-root UID, без підвищення привілеїв, з кореневою файловою системою лише для читання та записуваними тимчасовими каталогами. Наївна версія задає runAsNonRoot: true і readOnlyRootFilesystem: true, а потім дає збій, бо nginx очікує шляхи запису під час виконання. Успішна версія поєднує кожне обмеження з мінімальною підтримувальною конфігурацією.

Перше рішення — ідентичність. Оскільки завдання вимагає не-root поведінки, задайте runAsNonRoot: true і вкажіть числовий runAsUser, а не покладайтеся на типове значення образу. Друге рішення — записувані шляхи. Оскільки коренева файлова система лише для читання, додайте монтування emptyDir для шляхів, до яких nginx пише під час виконання. Третє рішення — привілеї. Оскільки програмі не потрібні додаткові повноваження ядра, вимкніть підвищення привілеїв і відкиньте всі можливості.

Цей приклад не просить вас запам’ятати внутрішню будову nginx. Він навчає діагностичної форми, яка застосовна до багатьох образів: після того, як ви замкнете ідентичність і кореневу файлову систему, стежте за конкретними шляхами, що все одно потребують записів під час виконання. Монтуйте лише ці шляхи, тримайте решту образу незмінною й лишайте додавання можливостей осторонь, доки завдання не дасть вам конкретної причини.

Підсумковий маніфест є наведеним раніше прикладом безпечного Пода й навмисно дружній до CKAD. Він використовує один Под, один контейнер, чіткі числові ідентифікатори та явні монтування томів. Під час перевірки ви можете запустити kubectl logs secure-pod, щоб підтвердити старт процесу, kubectl exec secure-pod -- id, щоб підтвердити ідентичність, та kubectl exec secure-pod -- touch /test, щоб підтвердити, що корінь лишається лише для читання. Ці перевірки пов’язують YAML зі спостережуваною поведінкою.

Ті самі міркування застосовні й тоді, коли ви ремонтуєте наявний маніфест. Прочитайте поточні поля, визначте, якої вимоги бракує, і зробіть найменшу зміну, що задовольняє цю вимогу без послаблення іншої. Додавання emptyDir до /tmp є малою зміною. Прибирання readOnlyRootFilesystem, бо один шлях потребує записів, є широким послабленням, а широкі послаблення рідко є правильною відповіддю на екзамені чи в огляді.

Контекст політики та сортування на екзамені

Розділ «Контекст політики та сортування на екзамені»

SecurityContext — це конфігурація робочого навантаження, а не політика кластера, але ці два аспекти зустрічаються під час допуску. Простір імен може мати політику допуску, яка перевіряє Под перед його плануванням, і Под, що порушує цю політику, може ніколи не створити запущеного контейнера. Саме тому перше запитання налагодження — чи Kubernetes відхилив специфікацію Пода, чи процес контейнера стартував і потім дав збій. Те саме поле YAML може бути частиною обох історій, але докази з’являються в різних місцях.

Pod Security Admission у Kubernetes використовує мітки простору імен, щоб застосувати Pod Security Standards, як-от privileged, baseline та restricted. Як кандидат CKAD, ви не маєте адмініструвати всю систему допуску, але маєте писати робочі навантаження, що можуть вижити в обмежених середовищах. Коли простір імен примушує профіль restricted, налаштування на кшталт privileged: true, широких можливостей, відсутніх засобів контролю не-root і підвищення привілеїв стають чимось більшим за зауваження в огляді. Вони можуть стати жорсткими причинами відхилення ще до того, як долучиться планувальник.

Профіль restricted корисний, бо перекладає широку мету безпеки на конкретні перевірки маніфесту. Він не просто каже «будь безпечнішим»; він шукає конкретні налаштування високого ризику. Привілейований контейнер відхиляється, бо отримує широкий доступ рівня хоста. Контейнер, що дозволяє підвищення привілеїв, відхиляється, бо може отримати більше привілеїв після старту. Правила можливостей підштовхують вас до відкидання ALL і додавання назад лише вузько дозволених винятків, як-от NET_BIND_SERVICE.

Помилки допуску часто є дієвішими, ніж здаються спершу. Вони зазвичай називають шлях поля, заборонене значення чи родину засобів контролю, що зазнала невдачі. Якщо повідомлення вказує на securityContext.privileged, приберіть привілейований режим і вирішіть, яку меншу можливість, якщо взагалі якусь, насправді потребує робоче навантаження. Якщо повідомлення вказує на додавання можливостей, порівняйте запитану можливість із профілем restricted. Якщо воно вказує на виконання від не-root, перегляньте, чи задає Под runAsNonRoot, runAsUser або перевизначення контейнера, що суперечить типовому значенню Пода.

Помилки виконання розповідають іншу історію, бо контейнер уже стартував. Рядок логу про невдачу запису файлу кешу — це не збій допуску; це процес зустрічається з правами Linux. Збій після увімкнення кореневої файлової системи лише для читання не є доказом того, що політика занадто сувора; це доказ того, що маніфест не оголосив усіх записуваних шляхів виконання. Тримання цих категорій окремо не дає вам виправляти проблему шляху виконання послабленням засобів контролю безпеки, звернених до допуску.

Сценарій вправи: Под приймається в просторі імен розробки, але відхиляється в обмеженому тренувальному просторі імен. Маніфест працює від UID 1000, але один сайдкар задає allowPrivilegeEscalation: true і додає SYS_PTRACE для налагодження. Правильний ремонт — не зміна ідентичності головного контейнера. Правильний ремонт — прибрати або обґрунтувати налаштування привілеїв сайдкара, бо відхилення походить від винятку рівня контейнера, що порушує політику простору імен.

Ті самі міркування допомагають із попередженнями. Деякі кластери налаштовують мітки Pod Security у режимі warn чи audit, перш ніж примусово застосувати політику. Попередження — це не пропуск; це попередній перегляд майбутнього відхилення. Якщо ви бачите попередження про поле SecurityContext під час практики, сприймайте його як безкоштовну підказку. Відремонтуйте маніфест, доки робоче навантаження ще стартує, а потім переконайтеся, що попередження зникло, перш ніж покладатися на Под у суворішому просторі імен.

Робота для Kubernetes 1.35+ має використовувати термінологію Pod Security Admission, а не старіші припущення про PodSecurityPolicy. PodSecurityPolicy було видалено з Kubernetes роками раніше, тож сучасна відповідь CKAD не повинна залежати від створення чи прив’язки об’єкта PSP. Для авторів робочих навантажень практичний підсумок простіший: пишіть специфікації Подів, які напряму задовольняють політику простору імен. Поля SecurityContext — це засоби контролю з боку робочого навантаження, які ви можете редагувати, не потребуючи прав адміністратора кластера.

Коли завдання на екзамені згадує обмежений простір імен, читайте це як обмеження на проєктування маніфесту. Почніть із не-root ідентичності, уникайте привілейованого режиму, вимкніть підвищення привілеїв, відкиньте можливості й надайте явні записувані шляхи, якщо вмикаєте кореневу систему лише для читання. Потім перевірте, чи завдання просить конкретного винятку. Якщо ні — не додавайте його. Маніфест, що проходить без винятку, легше пояснити, і він менш імовірно зіткнеться з допуском.

Є одна тонка пастка: допуск може валідувати поля, не знаючи кожної проблеми прав на файли під час виконання. Под може задовольняти політику restricted і все одно дати збій, бо образ пише до каталогу, яким володіє root. Це не збій політики; це збій контракту образу та файлової системи. Ремонт — змонтувати потрібний записуваний шлях, налаштувати програму писати в доречне місце або перебудувати образ із власністю, що відповідає налаштованому UID.

Використовуйте двостовпцевий ментальний чек-лист, навіть якщо не записуєте його. З боку політики запитайте, чи використовує маніфест заборонені привілеї, чи дозволяє підвищення, чи працює від root, чи зберігає широкі можливості. З боку виконання запитайте, чи може ідентичність процесу читати й писати ті шляхи, які йому насправді потрібні. Безпечний Под має задовольняти обидва стовпці. Проходження допуску є необхідним, але не доводить, що програма має правильні записувані каталоги.

Цей контекст політики також пояснює, чому SecurityContext належить до оглядів програм. Поля близькі до робочого навантаження, але виражають загальнокластерні очікування щодо радіуса ураження та ізоляції. Рецензент може порівняти оголошений виняток із потребою програми: вебсерверу може знадобитися записуваний каталог кешу, але йому не має знадобитися SYS_TIME; сайдкару метрик може знадобитися змонтований сокет, але він не має автоматично отримувати привілейований режим. Ця дисципліна огляду перетворює поля YAML на дієві інженерні рішення.

Патерни та антипатерни

Розділ «Патерни та антипатерни»

Патерни SecurityContext корисні лише тоді, коли зберігають конкретну властивість. «Захистити все» занадто розпливчасто, щоб скеровувати редагування маніфесту, тож кожен патерн нижче називає ситуацію, де він застосовний, і компроміс, який вводить. Спільна тема — явні найменші привілеї: вкажіть користувача, вкажіть записувані шляхи, вкажіть можливості й дайте Kubernetes відхиляти конфігурації, що порушують контракт.

ПатернКоли використовуватиЧому працюєЗауваження щодо масштабування
Типова ідентичність рівня ПодаУсі контейнери можуть розділяти той самий UID, GID і групу томаЗменшує повторюваний YAML, зберігаючи контракт Пода видимимСайдкари з іншою власністю образу можуть потребувати перевизначень контейнера
Коренева система лише для читання плюс іменовані записувані монтуванняПрограмі потрібні робочі шляхи виконання, але вона не повинна змінювати файли образуТримає шар образу незмінним, дозволяючи очікувані записиДокументуйте кожен записуваний шлях, щоб пізніші зміни не додавали прихованого стану
Відкинути всі можливості й додати одну назадРобоче навантаження потребує одного іменованого дозволу ядраРобить виняток придатним для огляду й уникає типових значень виконанняДодавання можливостей мають бути прив’язані до вимоги програми
Перевірка виконання після редагувань маніфестуПоля безпеки змінювалися під тиском часуПідтверджує чинний UID, групи й записувані шляхиАвтоматизуйте перевірки в CI для повторюваних навантажень, але знайте ручні команди

Антипатерни зазвичай походять зі спроби виправити симптом без визначення рівня, що його спричинив. Под, який не може писати до змонтованого тома, не потребує автоматично root. Контейнер, якому потрібна прив’язка порту, не потребує автоматично привілейованого режиму. Збій запуску після runAsNonRoot не означає, що налаштування погане; він означає, що образ чи маніфест усе ще хоче root.

АнтипатернЩо йде не такКраща альтернатива
Задавання privileged: true для виправлення невідомих помилокНадає широкий доступ рівня хоста й приховує справжній збійСпершу перегляньте події, UID, групи, шляхи та можливості
Використання runAsNonRoot без числового UIDОбраз усе ще може розв’язатися в root або бути неперевірюванимПоєднайте його з runAsUser: 1000 чи іншим відомим ненульовим UID
Вимкнення readOnlyRootFilesystem заради одного шляху записуРобить весь шар образу знову зміннимЗмонтуйте emptyDir чи відповідний том на потрібному шляху
Додавання широких можливостей до тестуванняДає процесу повноваження ядра, яких він може не потребуватиВідкиньте всі можливості, потім додайте лише іменовану вимогу

Ці патерни масштабуються, бо тримають усунення несправностей локальним. Коли Под дає збій, ви можете запитати, яка обіцянка порушена: ідентичність, записуваний шлях, можливість чи підвищення привілеїв. Коли відповідь локальна, виправлення зазвичай теж локальне. Ця звичка важлива у більших кластерах, де швидке, але широке послаблення може створити виняток політики, що переживе початковий інцидент.

Структура прийняття рішень

Розділ «Структура прийняття рішень»

Використовуйте цю структуру, коли вирішуєте, де розмістити налаштування або як відремонтувати збій. Мета — не зробити кожне робоче навантаження однаковим. Мета — зробити кожне робоче навантаження достатньо явним, щоб Kubernetes, рецензенти й майбутні оператори могли зрозуміти межу безпеки. Рухайтеся від ідентичності до файлової системи й до привілеїв, бо цей порядок відповідає найпоширенішим симптомам CKAD.

Start with the symptom
|
v
Does the pod fail before the process starts?
|
+-- yes --> Check events for runAsNonRoot, UID, policy, or validation errors
|
+-- no --> Can the process write the path it needs?
|
+-- no --> Check id, ls -la, fsGroup, and writable volume mounts
|
+-- yes --> Does the app need a kernel-level operation?
|
+-- yes --> Drop ALL capabilities, add the named capability
|
+-- no --> Keep privilege escalation false and root read-only

Перша гілка відокремлює відхилення конфігурації від поведінки програми. Якщо контейнер не стартує, kubectl exec поки не допоможе, тож події та огляд маніфесту йдуть першими. Якщо процес стартує й потім логує помилки прав доступу, ядро вже примушує правила ідентичності й файлової системи, тож ваші докази мають походити з id, власності на каталоги й конкретної цілі запису.

ЗапитанняОберіть цеУникайте цього
Чи всі контейнери розділяють той самий UID і GID?runAsUser і runAsGroup рівня ПодаПовторення однакових значень у кожному контейнері без причини
Чи потрібен одному контейнеру інший UID?Перевизначення runAsUser рівня контейнераЗміна типового значення Пода й поломка інших контейнерів
Чи потрібен змонтованому тому груповий доступ на запис?fsGroup рівня ПодаСпроба задати fsGroup під одним контейнером
Чи потрібен образу /tmp, поки корінь лише для читання?emptyDir, змонтований на /tmpГлобальне вимкнення readOnlyRootFilesystem
Чи потрібен процесу один привілей ядра?Відкиньте ALL, додайте іменовану можливістьЗадавання privileged: true

Структура також підказує, коли SecurityContext не є єдиною відповіддю. Якщо образ пише в жорстко закодовані каталоги програми, якими володіє root, маніфест може змонтувати поверх цих шляхів, але кращим довгостроковим виправленням може бути перебудова образу з правильною власністю й налаштовуваними шляхами. Kubernetes може примусити безпечніше середовище виконання, проте дизайн образу все одно визначає, наскільки легко цього середовища досягти.

Заради швидкості на CKAD запам’ятовуйте порядок рішень, а не кожне можливе поле. Збої ідентичності потребують runAsUser, runAsGroup і runAsNonRoot. Збої прав на томи вказують на fsGroup і власність на каталоги. Збої файлової системи лише для читання вказують на явні записувані монтування. Запити привілеїв вказують спершу на можливості, а на привілейований режим — лише тоді, коли робоче навантаження справді є системним компонентом рівня вузла.

  • Kubernetes документує SecurityContext окремо для Подів і контейнерів, бо PodSecurityContext і SecurityContext — це різні об’єкти API з полями, що перетинаються, але не є тотожними.
  • NET_BIND_SERVICE існує, бо Unix-подібні системи історично обмежували прив’язку до портів нижче 1024 привілейованими процесами; деякі сучасні середовища виконання послаблюють це через ip_unprivileged_port_start, але CKAD усе одно очікує цю можливість, коли завдання вимагає низького порту.
  • allowPrivilegeEscalation: false відображає ідею Linux no_new_privs, яка не дає процесу отримати привілеї через переходи exec.
  • fsGroup впливає на підтримувані змонтовані томи, а не на звичайні файли, уже запечені в шар образу контейнера.

Більшість помилок SecurityContext — це малі вибори в YAML із великими ефектами під час виконання. Виправлення зазвичай полягає не в прибиранні засобів контролю безпеки, а в розміщенні правильного засобу контролю на правильному рівні та перевірці результату однією конкретною командою. Використовуйте цю таблицю як чек-лист налагодження, коли Под дає збій під обмежувальною політикою чи стартує з помилками прав на файли.

ПомилкаЧому це стаєтьсяЯк це виправити
runAsNonRoot з образом, що все одно працює від rootПрапорець валідує не-root поведінку, але не обирає UIDДодайте числовий ненульовий runAsUser, потім перевірте через kubectl exec POD -- id
readOnlyRootFilesystem без записуваних монтувань виконанняОбраз пише тимчасові файли, сокети, кеші чи PID-файли під кореневими шляхамиЗмонтуйте томи emptyDir на точні записувані шляхи, як-от /tmp чи /var/run
Не відкидання можливостей перед додаванням однієїТипові значення виконання можуть лишити більше повноважень ядра, ніж потребує навантаженняЗадайте capabilities.drop: ["ALL"], потім додайте лише іменовану можливість
Плутання контексту рівня Пода й рівня контейнераДеякі поля перетинаються, тоді як інші чинні лише на одному рівніРозмістіть спільні типові значення й fsGroup на рівні Пода; розмістіть засоби контролю привілеїв на рівні контейнера
Використання privileged: true як загального виправленняВоно обходить багато звичайних меж ізоляції й приховує справжню вимогуВизначте, чи навантаження потребує зміни ідентичності, файлової системи чи однієї можливості
Задавання fsGroup для виправлення прав шару образуfsGroup націлений на підтримувані томи, а не на файли, запечені в образПеребудуйте образ із правильною власністю або змонтуйте записуваний том на шляху
Ігнорування подій, коли Под не стартує взагаліПоки немає процесу контейнера для огляду, тож exec не може працюватиВикористайте kubectl describe pod і прочитайте причину збою перед зміною YAML
Запитання 1: Ваш Под задає `runAsNonRoot: true`, але контейнер дає збій із повідомленням, що образ запуститься від root. У чому проблема і яку зміну маніфесту вам слід зробити?

runAsNonRoot: true — це перевірка валідації, а не призначення UID. Kubernetes відмовляється стартувати контейнер, бо чинний користувач образу — root або не може бути прийнятий як не-root. Додайте явний числовий ненульовий runAsUser, як-от 1000, на рівні Пода чи контейнера, що відповідає навантаженню. Якщо образ потім стикається з помилками прав на файли, збережіть налаштування не-root і відремонтуйте записувані шляхи чи власність образу замість прибирання запобіжника.

Запитання 2: Після увімкнення `readOnlyRootFilesystem: true` контейнер nginx дає збій під час запису `/var/cache/nginx`. Як зберегти перевагу безпеки й змусити Под стартувати?

Збережіть кореневу файлову систему лише для читання й змонтуйте записуваний том на конкретному шляху, що потребує записів під час виконання. Для цієї форми образу доречний emptyDir на /var/cache/nginx, а маніфест також може потребувати записуваних монтувань на /tmp і /var/run. Вимкнення readOnlyRootFilesystem зробило б весь шар образу знову змінним, що є ширшою зміною, ніж вимагає симптом. Правильне виправлення зберігає обмеження й документує очікувані записувані каталоги.

Запитання 3: Под має `spec.securityContext.runAsUser: 1000`. Контейнер A не має SecurityContext контейнера, тоді як Контейнер B задає `runAsUser: 2000`. Який UID використовує кожен контейнер і що вам слід оглянути далі, якщо спільний том не є записуваним?

Контейнер A успадковує UID 1000 від SecurityContext рівня Пода, тоді як Контейнер B працює від UID 2000, бо поле рівня контейнера перевизначає типове значення Пода для цього контейнера. Якщо спільний том не є записуваним, перегляньте fsGroup рівня Пода, групи процесу через id і власність на каталог через ls -la. Перевизначення UID пояснює ідентичність процесу, але доступ до тома часто залежить від власності на групу. Зміна обох контейнерів на root приховала б справжню проблему прав на том.

Запитання 4: Вашій команді потрібен контейнер для прив'язки до низького порту, і хтось пропонує `privileged: true`. Який підхід SecurityContext дотримується найменших привілеїв?

Не використовуйте привілейований режим для вузької вимоги прив’язки порту. Спершу відкиньте всі можливості, потім додайте NET_BIND_SERVICE, якщо процес справді має прив’язатися до порту нижче 1024. Це надає конкретний дозвіл ядра без широкого доступу до хоста. Вам також варто розглянути, чи може програма слухати на вищому порту контейнера й дозволити Сервісу виставити потрібний порт, бо повне уникнення можливості ще простіше, коли програма це підтримує.

Запитання 5: Под успішно стартує від UID `1000`, але `touch /data/file` дає збій із permission denied на `emptyDir`, змонтованому на `/data`. Що ви перевіряєте й змінюєте?

Спершу запустіть id усередині контейнера й ls -la /data, щоб порівняти UID і групи процесу з власністю й режимом каталогу. Якщо каталог потребує групового доступу для Пода, задайте fsGroup рівня Пода, що відповідає бажаній власності на групу. fsGroup — це правильний важіль для підтримуваних змонтованих томів, тоді як runAsUser керує UID процесу. Якщо шлях був би всередині образу, а не томом, доречнішим було б виправлення власності образу чи записуване монтування.

Запитання 6: Ви оглядаєте маніфест і знаходите `fsGroup` під `containers[0].securityContext`. Чому це підозріло і де має жити це налаштування?

fsGroup обмежений областю Пода, бо впливає на підтримувані змонтовані томи для Пода, а не лише на процес одного контейнера. Він належить до spec.securityContext, поряд з іншими типовими значеннями рівня Пода. SecurityContext рівня контейнера — це місце для полів на кшталт allowPrivilegeEscalation, readOnlyRootFilesystem і змін можливостей окремого контейнера. Переміщення fsGroup на рівень Пода робить область дії відповідною ресурсу, який налаштовується.

Запитання 7: Обмежений простір імен відхиляє Под, що задає `privileged: true`, але навантаженню потрібно лише оглянути власну ідентичність процесу й писати тимчасові файли. Як би ви переробили Под?

Приберіть privileged: true, бо описаному навантаженню не потрібен доступ рівня хоста. Задайте не-root UID через runAsUser і runAsNonRoot, вимкніть підвищення привілеїв, відкиньте всі можливості й увімкніть кореневу файлову систему лише для читання, якщо образ це підтримує. Додайте emptyDir на /tmp чи будь-який інший задокументований тимчасовий шлях. Цей дизайн задовольняє фактичні потреби, водночас узгоджуючись з очікуваннями політики restricted.

Ця вправа перетворює рішення модуля на спостережувану поведінку Пода. Запускайте команди в одноразовому просторі імен, якщо ви використовуєте спільний кластер, і видаляйте кожен Под перед переходом далі, якщо у вашому середовищі жорсткі квоти на ресурси. Маніфести використовують BusyBox і nginx, бо вони роблять ідентичність, записи у файли та каталоги виконання легкими для огляду без додаткового коду програми.

Завдання просуваються від ідентичності до власності на томи, до поведінки файлової системи лише для читання й нарешті до комбінованого безпечного Пода. Для кожного завдання спрогнозуйте результат перед запуском команди перевірки. Ця звичка важлива, бо робота з SecurityContext легша, коли ви знаєте, чи перевіряєте UID, власність на групу, записувані шляхи чи можливості.

Завдання: налаштувати параметри безпеки для Подів.

  • Впровадити runAsUser і runAsGroup рівня Пода, потім перевірити ідентичність процесу через логи.
  • Діагностувати, як fsGroup змінює власність на змонтований том для не-root процесу.
  • Довести, що readOnlyRootFilesystem блокує записи до шару образу, тримаючи Под запущеним.
  • Оцінити зміну лише можливостей, відкинувши всі можливості й додавши NET_BIND_SERVICE.
  • Спроєктувати придатний для CKAD маніфест безпечного Пода з не-root ідентичністю, кореневою системою лише для читання, відкинутими можливостями й записуваним змонтованим шляхом.

Частина 1: Запуск від не-root

Розділ «Частина 1: Запуск від не-root»

Перший Под — це мінімальна перевірка ідентичності. Він задає UID і первинний GID на рівні Пода, запускає id, а потім спить, щоб ви могли оглянути його за потреби. Очікуваний вивід має показати налаштовані UID і GID, що доводить: ідентичність процесу походить із SecurityContext, а не з типового значення образу.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: nonroot-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
containers:
- name: app
image: busybox
command: ['sh', '-c', 'id && sleep 3600']
EOF
kubectl wait --for=condition=ready pod/nonroot-pod --timeout=30s
kubectl logs nonroot-pod
# Should show: uid=1000 gid=3000 groups=3000
Нотатки до розв'язання для Частини 1

Якщо Под стає готовим, kubectl logs nonroot-pod має показати UID 1000 і GID 3000. Якщо ні — перегляньте kubectl describe pod nonroot-pod на проблеми завантаження образу чи планування перед зміною SecurityContext. Це завдання ізолює лише ідентичність процесу; воно не перевіряє fsGroup, бо том не змонтовано.

Частина 2: Демонстрація fsGroup

Розділ «Частина 2: Демонстрація fsGroup»

Другий Под додає змонтований emptyDir і fsGroup рівня Пода. Це найменша корисна демонстрація різниці між ідентичністю процесу й власністю на том. Процес усе ще працює від UID 1000, але змонтований каталог має показати групову поведінку, пов’язану з 2000.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: fsgroup-pod
spec:
securityContext:
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
image: busybox
command: ['sh', '-c', 'ls -la /data && id && sleep 3600']
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
EOF
kubectl wait --for=condition=ready pod/fsgroup-pod --timeout=30s
kubectl logs fsgroup-pod
# Files in /data owned by group 2000
Нотатки до розв'язання для Частини 2

Вивід логу має показати каталог /data й ідентичність процесу. Шукайте групу 2000 у власності чи списку груп. Якщо ваш драйвер зберігання обробляє власність інакше, зосередьтеся на концепції: fsGroup оголошується на рівні Пода, бо змонтовані томи є ресурсами Пода, і ви перевіряєте його, порівнюючи id з ls -la.

Частина 3: Файлова система лише для читання

Розділ «Частина 3: Файлова система лише для читання»

Третій Под навмисно намагається писати до кореневої файлової системи. Команда перехоплює збій і виводить чітке повідомлення, тож Под усе одно має стати готовим. Це дає вам безпечний спосіб довести, що коренева файлова система лише для читання, не перетворюючи вправу на завдання з налагодження циклу збоїв.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: readonly-pod
spec:
containers:
- name: app
image: busybox
command: ['sh', '-c', 'touch /test 2>&1 || echo "Cannot write!"; sleep 3600']
securityContext:
readOnlyRootFilesystem: true
EOF
kubectl wait --for=condition=ready pod/readonly-pod --timeout=30s
kubectl logs readonly-pod
# Should show: Cannot write!
Нотатки до розв'язання для Частини 3

Повідомлення підтверджує, що записи до /test заблоковані кореневою файловою системою лише для читання. Це очікувано й бажано. Якщо програмі справді потрібен записуваний шлях, змонтуйте том на цьому шляху замість вимкнення налаштування безпеки для всієї кореневої файлової системи.

Приберіть перші три Поди перед запуском вправ-дрилів, якщо у вашому кластері жорсткі квоти на Поди. Прапорці примусового видалення прийнятні для цієї одноразової вправи, бо Поди не керують довговічним станом. У продакшн-просторі імен віддавайте перевагу звичайному видаленню, якщо у вас немає причини обходити коректне завершення.

Terminal window
kubectl delete pod nonroot-pod fsgroup-pod readonly-pod --force --grace-period=0

Дриль 1: Запуск від користувача

Розділ «Дриль 1: Запуск від користувача»

Цей дриль повторює призначення UID без додаткового поля групи. Його мета — швидкість: ви маєте змогти створити маніфест, дочекатися готовності, прочитати лог і видалити Под, не шукаючи розташування полів. Сигнал перевірки — вивід id.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill1
spec:
securityContext:
runAsUser: 1000
containers:
- name: app
image: busybox
command: ['sh', '-c', 'id && sleep 3600']
EOF
kubectl wait --for=condition=ready pod/drill1 --timeout=30s
kubectl logs drill1
kubectl delete pod drill1 --force --grace-period=0

Дриль 2: Примус не-root

Розділ «Дриль 2: Примус не-root»

Цей дриль поєднує runAsNonRoot з явним UID, щоб запобіжник мав конкретну не-root ідентичність для примусу. Якщо ви приберете runAsUser, поведінка може залежати від оголошеного користувача образу. З наявним UID специфікація Пода чітко повідомляє вимогу не-root.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill2
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: app
image: busybox
command: ['sleep', '3600']
EOF
kubectl wait --for=condition=ready pod/drill2 --timeout=30s
kubectl get pod drill2
kubectl delete pod drill2 --force --grace-period=0

Дриль 3: Можливості

Розділ «Дриль 3: Можливості»

Цей дриль демонструє патерн можливостей з найменшими привілеями. Контейнер відкидає кожну можливість і додає назад лише NET_BIND_SERVICE. Вивід /proc/1/status закодований, тож не сприймайте його як зрозумілий новачку звіт; використовуйте його як доказ того, що стан можливостей спостережний зсередини контейнера.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill3
spec:
containers:
- name: app
image: busybox
command: ['sleep', '3600']
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
EOF
kubectl wait --for=condition=ready pod/drill3 --timeout=30s
kubectl exec drill3 -- cat /proc/1/status | grep Cap
kubectl delete pod drill3 --force --grace-period=0

Дриль 4: Лише для читання з тимчасовим каталогом

Розділ «Дриль 4: Лише для читання з тимчасовим каталогом»

Цей дриль поєднує кореневу файлову систему лише для читання із записуваним монтуванням /tmp. Команда пише до /tmp, а не до кореня образу, тож вона має успішно виконатися. Це той патерн, який ви використовуватимете щоразу, коли програмі потрібен робочий простір, але решта файлової системи має лишатися незмінною.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill4
spec:
containers:
- name: app
image: busybox
command: ['sh', '-c', 'touch /tmp/test && echo "Wrote to /tmp" && sleep 3600']
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
EOF
kubectl wait --for=condition=ready pod/drill4 --timeout=30s
kubectl logs drill4
kubectl delete pod drill4 --force --grace-period=0

Дриль 5: Перевірка fsGroup

Розділ «Дриль 5: Перевірка fsGroup»

Цей дриль пише файл у змонтований том і перелічує каталог. Він допомагає пов’язати fsGroup із файлами, створеними під монтуванням тома. Якщо вивід відрізняється між бекендами зберігання, поясніть різницю в термінах поведінки власності на том, а не змінюйте Под на root.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill5
spec:
securityContext:
fsGroup: 2000
containers:
- name: app
image: busybox
command: ['sh', '-c', 'touch /data/file && ls -la /data && sleep 3600']
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
EOF
kubectl wait --for=condition=ready pod/drill5 --timeout=30s
kubectl logs drill5
# File should be owned by group 2000
kubectl delete pod drill5 --force --grace-period=0

Дриль 6: Повний безпечний Под

Розділ «Дриль 6: Повний безпечний Под»

Фінальний дриль поєднує частини в один безпечний Под. Він працює від не-root UID, надає групу для доступу до тома, вимикає підвищення привілеїв, робить кореневу файлову систему лише для читання й відкидає можливості. Змонтований шлях /data дає процесу навмисне записуване розташування без послаблення шару образу.

Terminal window
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: drill6
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 2000
containers:
- name: app
image: busybox
command: ['sh', '-c', 'id && ls -la /data && sleep 3600']
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
EOF
kubectl wait --for=condition=ready pod/drill6 --timeout=30s
kubectl logs drill6
kubectl get pod drill6 -o yaml | grep -A20 securityContext
kubectl delete pod drill6 --force --grace-period=0
Нотатки до розв'язання для Дрилю 6

Логи мають показати UID 1000, первинний GID 1000 і груповий доступ, що включає групу тома. Огляд YAML має показати налаштування безпеки і рівня Пода, і рівня контейнера. Якщо Под дає збій, спершу використайте kubectl describe pod drill6, потім перевірте, чи збій є валідацією ідентичності, поведінкою образу чи проблемою записуваного шляху.

Перевірка засвоєння

Розділ «Перевірка засвоєння»

Завдання CKAD, які явно вимагають прив’язки до порту нижче 1024, усе одно очікують, що ви додасте NET_BIND_SERVICE, а не покладатиметеся на цю поведінку sysctl середовища виконання.

Модуль 4.5: ServiceAccount — далі ви пов’яжете ідентичність Пода з доступом до API Kubernetes, де ServiceAccount визначають, ким може автентифікуватися робоче навантаження і які дозволи воно може запитувати всередині кластера.