От переводчика: контексты безопасности (security contexts) в Kubernetes позволяют настраивать параметры безопасности на уровне пода или контейнера. Некоторые из них вполне очевидны, другие — не совсем ясны и сбивают с толку. В этой статье Рафаэль Натали объясняет ключевые моменты.
Мы знаем, насколько важна безопасность при работе с Kubernetes. Поскольку все рабочие нагрузки выполняются на операционных системах хост-машин (узлов), их защита становится первостепенной задачей. Если злоумышленник сможет выбраться из изолированного контейнера и получит доступ к хосту, под угрозой окажется не только сам узел со всеми его подами, но и весь кластер, а потенциально — и вся корпоративная сеть.

В этой статье мы объединили два материала Рафаэля Натали, постаравшись создать практическое руководство по созданию многоуровневой системы защиты хостов в Kubernetes. Рассмотрим два ключевых эшелона обороны:
На уровне спецификаций Kubernetes: использование встроенных механизмов, таких как Security Context, для ограничения прав и возможностей контейнеров.
На уровне ядра Linux: интеграция мощных инструментов безопасности, таких как AppArmor и seccomp, для детального контроля за взаимодействием подов с операционной системой хоста.
Приведённые рекомендации и примеры помогут вам значительно укрепить безопасность инфраструктуры и минимизировать риски, связанные с компрометацией контейнеризированных приложений.
Защита на уровне Kubernetes
Безопасность Kubernetes крайне важна, и начинать следует с защиты хоста Kubernetes от запущенных на нём контейнеров. Если злоумышленник взломает контейнер или под, у него появится несколько лазеек для атаки на сами хосты. А если удастся взломать операционную систему хоста, злоумышленник сможет через неё атаковать другие узлы кластера (не говоря уже обо всех подах и приложениях, работающих на этом конкретном узле). В наихудшем сценарии он сможет добраться и до других систем в вашей сети. В следующих разделах речь пойдёт о том, как обезопасить операционную систему хоста.
Пространства имён операционной системы
С точки зрения безопасности, контейнеры задействуют пространства имён операционной системы. Это понятие не следует путать с пространствами имён (namespaces) Kubernetes. В данном контексте пространства имён — функция ядра Linux, которую контейнерные технологии используют для того, чтобы отделять один контейнер от прочих, работающих на той же машине, а также от операционной системы хоста.
На сервере Kubernetes имеются пространства имён хоста. Их используют обычные приложения, работающие напрямую на хосте. Контейнеры, напротив, работают в собственных изолированных пространствах имён ядра Linux. Такая организация обеспечивает изоляцию как между контейнерами, так и между контейнерами и операционной системой хоста.

Если контейнер взломают, активность атакующего будет ограничена только областью этого контейнера (его пространством имён). Подобная изоляция мешает злоумышленнику получить доступ к хосту или другим контейнерам и взломать их.
Почему это важно? Дело в том, что поды можно настроить так, чтобы они использовали общее пространство имён хоста, а не своё, изолированное. Это может понадобиться, если контейнеру нужно напрямую «общаться» с операционной системой хоста. Но делать это стоит только в самых крайних случаях из-за рисков, связанных с безопасностью. Если злоумышленник взломает такой контейнер, у него появится больше возможностей для взаимодействия с компонентами операционной системы, ведь изоляция контейнера уже не будет стоять у него на пути.
Ниже приведена конфигурация, которая позволяет поду напрямую взаимодействовать с операционной системой хоста:
apiVersion: v1
kind: Pod
metadata:
name: host-pod
spec:
hostIPC: true
hostNetwork: true
hostPID: true
containers:
- name: nginx
image: nginx
Если установить параметр spec.hostIPC
в true
, контейнеры будут использовать пространство имён хоста для межпроцессного взаимодействия (IPC).
Межпроцессное взаимодействие — это механизм в Linux, который позволяет процессам общаться между собой. Обычно контейнеры используют собственное, отдельное IPC-пространство имён, что исключает связь между контейнерами и процессами на операционной системе хоста. Другая настройка, spec.hostNetwork
, управляет сетевым пространством имён. А spec.hostPID
указывает контейнеру использовать пространство идентификаторов процессов (PID) хоста. Все эти настройки предписывают контейнеру использовать соответствующее пространство имён хоста. По умолчанию все они установлены в false
. Если их не менять, контейнеры будут использовать изолированные пространства имён.
Не запускайте поды в привилегированном режиме
Ещё один важный момент, который нужно учитывать при защите операционной системы хоста, — это привилегированный запуск. Привилегированный режим даёт контейнеру самый высокий возможный уровень разрешений. С ним контейнеры могут повышать привилегии и получать доступ к ресурсам на уровне хоста — почти так же, как если бы процесс запускался напрямую на самом хосте.
На диаграмме ниже показаны непривилегированный и привилегированный контейнеры, запущенные на одном и том же хосте. В случае компрометации непривилегированного контейнера злоумышленник, как правило, не сможет получить доступ к ресурсам хоста. С привилегированными контейнерами ситуация иная — их взлом может предоставить атакующему прямой доступ к системе.

Чтобы создать привилегированный контейнер, установите поле spec.containers[*].SecurityContext.privileged
в true
в определении пода:
apiVersion: v1
kind: Pod
metadata:
name: privileged-pod
spec:
containers:
- name: nginx
image: nginx
securityContext:
privileged: true
Разбираемся с возможностями Linux
Третья концепция, которую нужно рассмотреть, — это возможности (capabilities) Linux. Они наделяют процесс конкретными привилегиями, не предоставляя ему полный набор прав суперпользователя (root). По умолчанию контейнеры работают с базовым набором возможностей, который задаётся средой исполнения контейнеров, и этого обычно хватает для большинства случаев. В соответствии с принципом минимальных привилегий желательно удалять все возможности из конфигурации пода, за исключением тех, которые ему действительно необходимы. Наделение пода избыточными правами может представлять угрозу для безопасности операционной системы хоста.
Чтобы явно отобрать все возможности у пода, установите параметр spec.containers[*].securityContext.capabilities.drop
в значение ALL
:
apiVersion: v1
kind: Pod
metadata:
name: capabilities-pod
spec:
containers:
- image: busybox
name: busybox
command:
- sleep
- "3600"
securityContext:
capabilities:
drop:
- ALL
Запускаем контейнеры не под root'ом
И последнее, о чём мы поговорим, — это runAsUser
. Контейнеры могут работать под любым пользователем Linux. Если контейнер работает под root’ом (то есть runAsUser: 0
) и его взломают, то злоумышленник, весьма вероятно, сможет получить root-доступ к операционной системе хоста. С таким доступом злоумышленник сможет захватить весь Kubernetes-кластер или другие системы компании.
Приведённая ниже конфигурация пода уменьшает риски запуска контейнеров под root'ом:
apiVersion: v1
kind: Pod
metadata:
name: capabilities-pod
spec:
containers:
- image: busybox
name: busybox
command:
- sleep
- "3600"
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
Если поле spec.containers[*].securityContext.runAsNonRoot
установлено в true
, контейнер будет запускаться от имени пользователя, не являющегося суперпользователем (non-root). Два других поля, spec.containers[*].securityContext.runAsUser
и spec.containers[*].securityContext.runAsGroup
, определяют пользователя и группу, под которыми будут запускаться приложения внутри контейнера. Чтобы гарантировать, что приложение будет работать от имени non-root-пользователя, помимо настроек в определении пода, укажите пользователя и группу в Dockerfile при сборке Docker-образа для приложения.
Усиление изоляции с помощью модулей ядра Linux
Некоторые защитные (hardening) инструменты ядра Linux можно интегрировать с Kubernetes, чтобы контролировать взаимодействие подов и контейнеров с операционной системой хоста. Например, можно запретить подам создавать файлы или запускать программы. Ниже мы рассмотрим два таких инструмента: AppArmor и seccomp. Покажем, как с их помощью ограничить некоторые операции между Kubernetes и операционной системой хоста.
AppArmor
AppArmor — модуль безопасности ядра Linux, который позволяет гранулярно настраивать права доступа для программ, работающих в системах Linux. Профиль AppArmor содержит правила, указывающие, что программе можно делать, а что нельзя.
Профиль AppArmor загружается на уровне сервера и может быть активирован в одном из двух режимов. Первый режим — complain
(режим уведомлений). В нем AppArmor ничего не блокирует, а только создаёт отчёт о действиях, которые выполняет программа. С его помощью можно выяснить, какие команды или функции запускает под. Второй режим — enforce
(режим принуждения). В нем AppArmor будет активно блокировать любые действия пода, которые не разрешены профилем. Важно, что профили AppArmor должны быть активированы на каждом воркере (worker-узле).
Применение профиля AppArmor к поду
Создадим профиль AppArmor, запрещающий все операции записи на диск, и применим данный профиль к поду. На worker-узле создайте файл k8s-deny-write со следующим содержимым:
#include <tunables/global>
profile k8s-deny-write flags=(attach_disconnected) {
#include <abstractions/base>
file,
# Запретить все операции записи в файлы.
deny /** w,
}
Активировать профиль AppArmor можно с помощью команды apparmor_parser
. По умолчанию профиль загружается в режиме enforce
. Для активации в режиме complain
используйте флаг -C
.
>sudo apparmor_parser ./k8s-deny-write
Проверить, загружен ли профиль, можно с помощью aa-status
:
>sudo aa-status
apparmor module is loaded.
56 profiles are loaded.
52 profiles are in enforce mode.
...
k8s-deny-write
...
0 processes are unconfined but have a profile defined.
С помощью следующего манифеста создадим под, который выводит простое сообщение:
apiVersion: v1
kind: Pod
metadata:
name: hello-apparmor
spec:
containers:
- name: hello
image: busybox:1.28
command: [ "sh", "-c", "while true; do echo 'Hello AppArmor!' > /tmp/hello && cat /tmp/hello; sleep 10; done" ]
После создания пода посмотрим его логи. Сообщение было записано:
>kubectl logs hello-apparmor -f
Hello AppArmor!
Hello AppArmor!
Примечание
До Kubernetes v1.30 AppArmor настраивался через аннотации. Информацию для предыдущих версий смотрите в документации.
Теперь настроим профиль AppArmor в манифесте пода через securityContext
. Новый манифест будет таким:
apiVersion: v1
kind: Pod
metadata:
name: hello-apparmor
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-deny-write
containers:
- name: hello
image: busybox:1.28
command: [ "sh", "-c", "while true; do echo 'Hello AppArmor!' > /tmp/hello && cat /tmp/hello; sleep 10; done" ]
Удалим текущий под, применим новый манифест и снова проверим логи:
>kubectl delete pods hello-apparmor
pod "hello-apparmor" deleted
>kubectl apply -f apparmor.yaml
pod/hello-apparmor created
>kubectl logs hello-apparmor -f
sh: can't create /tmp/hello: Permission denied
sh: can't create /tmp/hello: Permission denied
sh: can't create /tmp/hello: Permission denied
Под hello-apparmor не может создавать файлы из-за профиля AppArmor. Проверить, применился ли профиль, также можно с помощью следующей команды:
>kubectl exec hello-apparmor -- cat /proc/1/attr/current
k8s-deny-write (enforce)
Здесь видно, что профиль k8s-deny-write применился и работает в режиме enforce
.
В завершение раздела давайте посмотрим, что будет, если указать поду профиль, который не был загружен. Создадим такой под:
>kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
name: hello-apparmor-2
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-apparmor-example-allow-write
containers:
- name: hello
image: busybox:1.28
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
Проверим его статус:
>kubectl get pods hello-apparmor-2
NAME READY STATUS RESTARTS AGE
hello-apparmor-2 0/1 CreateContainerError 0 12s
Используем kubectl describe
, чтобы разобраться в ошибке:
>kubectl describe pods hello-apparmor-2
...
Warning Failed 79s (x12 over 3m15s) kubelet Error: failed to get container spec opts: failed to generate apparmor spec opts: apparmor profile not found k8s-apparmor-example-allow-write
...
В секции Events (События) чётко видно, что профиль AppArmor не найден. Таким образом, под не запустится, если профиль не был предварительно загружен на узле.
Seccomp
Seccomp — фича ядра Linux, которая позволяет ограничивать системные вызовы, доступные приложениям. С её помощью можно повышать безопасность приложений, ограничивая их взаимодействие с операционной системой, тем самым уменьшая потенциальную поверхность атаки. Для этого определяется фильтр, который указывает, какие системные вызовы разрешены, а какие — запрещены.
В Kubernetes seccomp можно использовать для определения политик безопасности подов. Так поды смогут использовать только необходимые для их работы системные вызовы, что снижает риск эксплуатации уязвимостей.
Примеры профилей seccomp
Скачаем три примера профилей seccomp для тестирования наших подов. Эти файлы должны находиться на каждом узле кластера Kubernetes. Первый профиль, audit.json, логирует все системные вызовы процесса. Второй, violation.json, запрещает любые системные вызовы. Третий, fine-grained.json, разрешает определённый набор системных вызовов в блоке action: SCMP_ACT_ALLOW
. Чтобы скачать и сохранить их в локальную директорию /var/lib/kubelet/seccomp/seccomp_profiles, выполните следующие команды:
>sudo mkdir -p /var/lib/kubelet/seccomp/seccomp_profiles
>curl -L -o seccomp_profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
>curl -L -o seccomp_profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
>curl -L -o seccomp_profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
>ls seccomp_profiles/
audit.json fine-grained.json violation.json
Создаём под, который логирует все системные вызовы
Сначала настроим профиль audit.json в поле .spec.securityContext
нашего пода. Ниже приведён манифест пода, который будет его использовать:
apiVersion: v1
kind: Pod
metadata:
name: audit-pod
labels:
app: audit-pod
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: seccomp_profiles/audit.json
containers:
- name: test-container
image: hashicorp/http-echo:1.0
args:
- "-text=just made some syscalls!"
securityContext:
allowPrivilegeEscalation: false
Создадим под:
>kubectl apply -f audit.yaml
pod/audit-pod created
Этот профиль ничего не блокирует, так что под запустится без проблем. Теперь подключимся к worker-узлу и посмотрим в syslog
, какие системные вызовы совершает audit-pod:
>sudo tail -f /var/log/syslog | grep 'http-echo'
Jul 23 22:09:48 5600791c132c kernel: [ 2070.311535] audit: type=1326 audit(1721772588.695:700): auid=4294967295 uid=65532 gid=65532 ses=4294967295 subj=cri-containerd.apparmor.d pid=16021 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=35 compat=0 ip=0x4685d7 code=0x7ffc0000
Jul 23 22:09:48 5600791c132c kernel: [ 2070.311662] audit: type=1326 audit(1721772588.695:701): auid=4294967295 uid=65532 gid=65532 ses=4294967295 subj=cri-containerd.apparmor.d pid=16021 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x468ba3 code=0x7ffc0000
Создаём под с профилем, который запрещает все системные вызовы
Профиль violation.json запрещает все системные вызовы. Если применить его к поду, последний не сможет запуститься. Под ниже как раз использует этот профиль:
apiVersion: v1
kind: Pod
metadata:
name: violation-pod
labels:
app: violation-pod
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: seccomp_profiles/violation.json
containers:
- name: test-container
image: hashicorp/http-echo:1.0
args:
- "-text=just made some syscalls!"
securityContext:
allowPrivilegeEscalation: false
Создадим его и проверим статус:
>kubectl apply -f violation.yaml
pod/violation-pod created
>kubectl get pods violation-pod
NAME READY STATUS RESTARTS AGE
violation-pod 0/1 RunContainerError 2 (1s ago) 26s
Заглянув в syslog
, увидим ошибку, которая говорит, что под не может запустить свой процесс:
>sudo tail -f /var/log/syslog | grep 'http-echo'
Jul 23 22:28:14 5600791c132c kubelet[769]: E0723 22:28:14.220131 769 kuberuntime_manager.go:1256] container &Container{Name:test-container,Image:hashicorp/http-echo:1.0,Command:[],Args:[-text=just made some syscalls!],WorkingDir:,Ports:[]ContainerPort{},Env:[]EnvVar{},Resources:ResourceRequirements{Limits:ResourceList{},Requests:ResourceList{},Claims:[]ResourceClaim{},},VolumeMounts:[]VolumeMount{VolumeMount{Name:kube-api-access-lmsz5,ReadOnly:true,MountPath:/var/run/secrets/kubernetes.io/serviceaccount,SubPath:,MountPropagation:nil,SubPathExpr:,RecursiveReadOnly:nil,},},LivenessProbe:nil,ReadinessProbe:nil,Lifecycle:nil,TerminationMessagePath:/dev/termination-log,ImagePullPolicy:IfNotPresent,SecurityContext:&SecurityContext{Capabilities:nil,Privileged:nil,SELinuxOptions:nil,RunAsUser:nil,RunAsNonRoot:nil,ReadOnlyRootFilesystem:nil,AllowPrivilegeEscalation:*false,RunAsGroup:nil,ProcMount:nil,WindowsOptions:nil,SeccompProfile:nil,AppArmorProfile:nil,},Stdin:false,StdinOnce:false,TTY:false,EnvFrom:[]EnvFromSource{},TerminationMessagePolicy:File,VolumeDevices:[]VolumeDevice{},StartupProbe:nil,ResizePolicy:[]ContainerResizePolicy{},RestartPolicy:nil,} start failed in pod violation-pod_default(9743bb58-804b-40c8-9775-babfa69e80d1): RunContainerError: failed to start containerd task "28a0542f2cb20f3747de7518a1bafeeb0c470bd0cbac07482f4bca82f1b32279": cannot start a stopped process: unknown
Как и ожидалось, профиль seccomp не дал поду выполнить ни одного системного вызова.
Создаём под с профилем, который позволяет выполнять необходимые системные вызовы
Чтобы образ http-echo
успешно запустился, ему нужно разрешение на выполнение некоторых системных вызовов. Профиль fine-grained.json разрешает необходимые системные вызовы. Ниже приведён пример манифеста пода, использующего этот профиль:
apiVersion: v1
kind: Pod
metadata:
name: fine-grained-pod
labels:
app: fine-grained-pod
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: seccomp_profiles/fine-grained.json
containers:
- name: test-container
image: hashicorp/http-echo:1.0
args:
- "-text=just made some syscalls!"
securityContext:
allowPrivilegeEscalation: false
Создадим под и проверим его статус:
>kubectl apply -f fine-grained.yaml
pod/fine-grained-pod created
>kubectl get pods fine-grained-pod
NAME READY STATUS RESTARTS AGE
fine-grained-pod 1/1 Running 0 67s
Так как профиль разрешает все системные вызовы, нужные для образа, в syslog
'е нет ошибок, и под запускается успешно.
Заключение
В статье рассмотрели комплексный подход к защите операционной системы хоста в среде Kubernetes, который является критически важным для обеспечения общей безопасности кластера. Надёжная защита строится по многослойному принципу и включает в себя минимум два уровня.
Первый — использование нативных средств Kubernetes. Применение принципа наименьших привилегий через Security Context является базовой гигиеной безопасности. Всегда следует:
избегать привилегированного режима и прямого доступа к пространствам имён хоста (
hostIPC
,hostNetwork
,hostPID
);ограничивать доступ к возможностям (capabilities) Linux, которые явно поду не требуются;
запускать контейнеры от имени пользователя без root-прав.
Второй уровень — усиление защиты с помощью специализированных модулей ядра Linux. Такие инструменты, как AppArmor и seccomp, позволяют гранулярно задавать политики безопасности, которые ограничивают доступ приложений к возможностям Linux (AppArmor) и системным вызовам (seccomp). Это значительно сокращает поверхность атаки, даже если злоумышленнику удастся найти уязвимость в приложении.
Совместное применение этих подходов создаёт мощный барьер, который эффективно изолирует контейнеры от хоста и друг от друга.
P. S.
Читайте также в нашем блоге: