Learning Platform
Глоссарий Troubleshooting
Урок 15.01 · 25 мин
Продвинутый
SecurityContextrunAsNonRootcapabilitiesreadOnlyRootFilesystemfsGroup

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.
Какое поле где живёт
Pod-levelPodSecurityContext. Поля живут в spec.securityContext. Применяются ко всем containers одновременно. Удобно для UID/GID и fsGroup, когда вы хотите единую identity для всего Pod.
Pod-levelАналог runAsUser, но для primary GID процессов в контейнерах.
Pod-levelДополнительные GIDs для процессов, помимо runAsGroup. Помогает дать процессу членство в supplementary group без смены primary GID.
Pod-levelОсобое поле: применяется к volumes Pod. Kubelet рекурсивно chown'ит файлы во volumes на этот GID, чтобы процесс мог писать. Только Pod-level.
Оба уровняrunAsNonRoot можно ставить и в Pod, и в container. Container-level override-ит Pod-level для конкретного container. На CKAD пишут обычно в Pod-level — короче и safer-by-default.
Оба уровняrunAsUser/runAsGroup тоже доступны на container-level — для случая когда один контейнер должен бежать под другим UID.
Оба уровняseccompProfile задаёт seccomp filter — список разрешённых syscalls. Pod-level применяется ко всем containers, container-level override.
Только containercapabilities — Linux capabilities (CAP_NET_BIND_SERVICE, CAP_SYS_TIME, ...). Capability bound process на конкретном container. Pod-level поля для capabilities просто нет — попытка положить туда дастся валидацией apiserver.
Только containerprivileged: true даёт контейнеру full host access (доступ ко всем устройствам, всем capabilities). Для CNI, GPU-runtime и monitoring агентов. В production app — никогда.
Только containerallowPrivilegeEscalation: false запрещает setuid binaries поднимать UID. Это no_new_privs bit в Linux. Должен быть false для production apps.
Только containerreadOnlyRootFilesystem делает / read-only внутри container. Запись возможна только в volumes (emptyDir для tmp/cache). Защищает от модификации binaries при компрометации.
Только containerprocMount: Default или Unmasked. Unmasked отключает masking путей в /proc — нужно для тестов, ломает изоляцию. Default — правильный выбор.
WARNING

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 — почти обязательный паттерн.
NOTE

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 это знание ценится.

WARNING

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. Избегать.
DANGER

capabilitiescontainer-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_privs Linux 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.
Защищённый Pod: что нужно поставить
Pod-levelБазовая identity — не root и явный UID. fsGroup для volumes. Pod-level потому что applies ко всем containers в Pod.
Container-levelContainer-level защита: read-only root FS, drop всех capabilities, отказ от privilege escalation. Это четыре поля, которые делают разницу между 'обычный Pod' и 'защищённый Pod'.
ОпциональноseccompProfile RuntimeDefault блокирует опасные syscalls. С Pod Security Standards restricted — это требование. На CKAD знать его существование.

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.


Проверка знанийKnowledge check
В spec.securityContext (Pod-level) положили capabilities: drop: [ALL]. Apiserver вернул ошибку. Почему?
ОтветAnswer
capabilities — поле только container-level. Тип PodSecurityContext не имеет этого поля. Нужно перенести в spec.containers[*].securityContext.capabilities. То же самое верно для privileged, allowPrivilegeEscalation, readOnlyRootFilesystem, procMount.
Проверка знанийKnowledge check
Pod указан runAsNonRoot: true, но runAsUser не задан. Image — официальный nginx (по умолчанию запускается под root). Что произойдёт при создании Pod?
ОтветAnswer
Pod упадёт с CreateContainerConfigError. Kubelet перед стартом проверяет effective UID; image не указывает non-root user, default — root (UID 0). Решение — либо использовать image, где явно USER non-root (nginxinc/nginx-unprivileged), либо добавить runAsUser: 1000 в SecurityContext.
Проверка знанийKnowledge check
PV содержит 5 миллионов файлов. При каждом старте Pod запуск занимает 3 минуты. Что в SecurityContext поможет?
ОтветAnswer
Установить fsGroupChangePolicy: OnRootMismatch на Pod-level. Default policy Always рекурсивно chown'ит весь volume на каждом mount. OnRootMismatch проверяет только GID корня volume — если совпадает с fsGroup, chown пропускается. Это десятки минут разницы для больших PV.
Проверка знанийKnowledge check
Какой минимальный набор полей делает Pod compatible с Pod Security Standard restricted?
ОтветAnswer
runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile.type: RuntimeDefault. runAsUser обычно явно задают (хорошая практика). readOnlyRootFilesystem рекомендуется, но не строгое требование restricted в API смысле — хотя многие enforcement-инструменты его требуют.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В spec.securityContext (Pod-level) положили capabilities: { drop: [ALL] }. Apiserver вернул ошибку валидации. Почему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5