SecurityContext: основа безопасности Pod
SecurityContext — это место в spec, где вы говорите кластеру, под каким UID/GID запускать процессы, какие Linux capabilities оставить, разрешать ли privilege escalation, должна ли корневая FS быть read-only. Это первая и самая дешёвая линия защиты на уровне Pod. Тема выглядит простой — много YAML-полей, — но за ней стоит важная архитектурная деталь: поля разделены на два уровня, и попытка поставить container-level поле в Pod-level spec (или наоборот) даёт молчаливое не-применение. На CKAD это пол-балла, а в production — это та самая причина «почему мой capabilities: drop ALL не сработал».
USER и безопасность: запуск контейнера без root
Два уровня: Pod-level vs container-level
SecurityContext существует в двух местах в spec:
apiVersion: v1
kind: Pod
spec:
securityContext: # <-- Pod-level (PodSecurityContext)
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
securityContext: # <-- container-level (SecurityContext)
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
Это два разных API-объекта с разными полями:
- PodSecurityContext (
spec.securityContext) — настройки уровня Pod. Применяются ко всем containers, init-containers и ephemeral-containers внутри Pod. - SecurityContext (
spec.containers[].securityContext) — настройки конкретного container. Если значение установлено и в Pod-level, и в container-level, container-level выигрывает для этого container.
Killer-момент CKAD: capabilities, privileged, allowPrivilegeEscalation, readOnlyRootFilesystem, procMount — только container-level. Положите их в spec.securityContext — apiserver их проигнорирует или отвергнет. fsGroup — наоборот, только Pod-level. runAsUser, runAsGroup, runAsNonRoot, seccompProfile — оба уровня, container override-ит Pod.
runAsUser, runAsGroup, runAsNonRoot
Эти поля управляют UID/GID, под которыми бегут процессы внутри контейнера.
spec:
securityContext:
runAsUser: 1000 # PID 1 и все его child'ы — UID 1000
runAsGroup: 3000 # primary GID — 3000
runAsNonRoot: true # fail-fast если эффективный UID == 0
- runAsUser — UID для PID 1 в контейнере. Override-ит
USERдирективу из Dockerfile. - runAsGroup — primary GID. Если не указан, наследуется от image (
/etc/passwd). - runAsNonRoot: true — kubelet проверяет на старте: если effective UID == 0, Pod упадёт с CreateContainerConfigError. Это safety-net: даже если image
USERзабыли — Pod не стартует. На CKAD — почти обязательный паттерн.
runAsNonRoot: true без runAsUser проверяет UID из image. Если image имеет USER 0 (root) — Pod упадёт. Если image не указывает USER (большинство публичных images типа nginx, postgres) — kubelet не может определить UID и тоже упадёт. Поэтому связка runAsNonRoot: true + явный runAsUser: 1000 — production-стандарт.
fsGroup и fsGroupChangePolicy
fsGroup — особое поле только Pod-level. Оно применяется не к процессам, а к volumes. Kubelet при mount volume рекурсивно меняет ownership на fsGroup, чтобы процесс под другим UID мог писать.
spec:
securityContext:
runAsUser: 1000
fsGroup: 2000 # все volumes chown'ятся в group 2000
fsGroupChangePolicy: OnRootMismatch
containers:
- name: app
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-pvc
fsGroupChangePolicy: OnRootMismatch — это performance optimization. Default policy Always chown’ит volume на каждом старте Pod. Для больших PV (миллионы файлов) это minutes of startup time. OnRootMismatch пропускает chown, если GID корня volume уже совпадает — обычное поведение для re-mount существующего PV. На CKAD это знание ценится.
fsGroup работает не для всех volume types. Для emptyDir, configMap, secret, projected, persistentVolumeClaim — работает. Для hostPath — НЕ работает (нет смысла менять ownership на хосте). Для NFS — поведение зависит от backend.
capabilities: точечный privilege
Linux capabilities — это разбивка root-привилегий на ~40 независимых битов. Container runtime по умолчанию дропает большинство, оставляя только CAP_CHOWN, CAP_NET_BIND_SERVICE, CAP_KILL и ещё штук десять — этого хватает большинству apps.
containers:
- name: app
securityContext:
capabilities:
drop: [ALL] # дропнуть всё
add: [NET_BIND_SERVICE] # вернуть только то, что нужно
Пишут обычно без CAP_ префикса — K8s сам добавит. Самые частые в production:
- NET_BIND_SERVICE — bind на порты < 1024. Нужен если хотите слушать :80 или :443 от non-root. Default capability container runtime обычно даёт.
- NET_ADMIN — настройка интерфейсов, iptables. Для service mesh sidecars, CNI agents.
- SYS_TIME — менять системное время. Почти никогда не нужно в Pod.
- SYS_ADMIN — гигантский capability, почти эквивалент root. Избегать.
capabilities — container-level only. Pod-level securityContext поля capabilities не имеет. Тип PodSecurityContext в Go-коде K8s даже не объявляет это поле. Если положили в Pod-level — apiserver вернёт unknown field "capabilities".
allowPrivilegeEscalation и readOnlyRootFilesystem
Два дешёвых полей, которые должны быть в любом production Pod:
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- allowPrivilegeEscalation: false — устанавливает
no_new_privsLinux bit. Это запрещает любой child процессу получать больше привилегий, чем родитель. Setuid-бинарники (sudo, ping в старых images) перестают работать. Это правильно: в Pod вам никогда не нужен sudo. - readOnlyRootFilesystem: true —
/смонтирована read-only. Если приложению нужны writable пути (/tmp,/var/run,/var/cache) — добавьте emptyDir mount. Защита: при компрометации attacker не может писать в/usr/bin/, заменить binary, оставить persistence.
AppArmor: appArmorProfile field (GA с v1.31)
Исторически AppArmor profile задавался через аннотации на Pod: container.apparmor.security.beta.kubernetes.io/<container-name>: runtime/default. С v1.31 это поле GA внутри securityContext (на Pod- и container-level), аннотация — deprecated и в новых манифестах не используется.
spec:
securityContext:
appArmorProfile:
type: RuntimeDefault # стандартный профиль из container runtime
containers:
- name: app
image: my-app
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: my-profile # имя файла в /etc/apparmor.d/ на ноде
Типы:
RuntimeDefault— профиль, который ставит runtime (containerd/cri-o). Рекомендуемый baseline.Localhost— кастомный профиль, заранее загруженный на ноду. Имя черезlocalhostProfile.Unconfined— без AppArmor. Не рекомендуется.
AppArmor работает только на Linux-нодах с включённой AppArmor (Ubuntu/Debian — да; в RHEL/CentOS чаще SELinux, который задаётся через seLinuxOptions). На CKAD сам синтаксис спрашивают редко, но знать что аннотации deprecated, поле — appArmorProfile — важно.
Полный production-grade Pod
Собираем всё вместе:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:1.2.3@sha256:abc...
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
add: [NET_BIND_SERVICE]
ports:
- containerPort: 8080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Этот Pod проходит Pod Security Standard restricted (об этом в уроке 4) и считается production-baseline практически для любой компании, где есть security review.