Модуль 13 / 18
Безопасность: rootless, capabilities, seccomp и LSM
Содержание
1. Цели и модель угроз
После модуля вы сможете применять least privilege слоями и диагностировать отказ без опасного --privileged.
- non-root уменьшает последствия
- capabilities дробят полномочия root
- seccomp ограничивает syscalls
- AppArmor/SELinux ограничивают доступ
- read-only rootfs и no-new-privileges сужают изменения
2. Теория: граница контейнера
Namespaces и cgroups не делают контейнер равным VM: процессы разделяют kernel attack surface. Docker daemon и доступ к docker.sock обычно дают root-equivalent контроль хоста.
--privileged выдаёт capabilities, devices и ослабляет LSM, поэтому не является исправлением Permission denied.
3. Теория: user и user namespaces
USER в image или --user задаёт UID/GID процесса, но root внутри обычного контейнера остаётся UID 0 хоста в своём namespace. Права bind mounts сравниваются численно.
userns-remap отображает container root в subordinate host UID. Rootless mode запускает daemon и containers без host root, но требует отдельного context и имеет ограничения network/storage/cgroup.
4. Теория: Linux capabilities
Docker сохраняет ограниченный default-набор capabilities. Начинайте с --cap-drop ALL и добавляйте одну доказанную capability. NET_RAW разрешает raw sockets, NET_ADMIN меняет network state, SYS_ADMIN слишком широк.
Capability не отменяет seccomp, LSM и filesystem permissions: доступ является пересечением всех слоёв.
5. Теория: seccomp и no-new-privileges
Default seccomp profile разрешает совместимый набор syscalls и возвращает EPERM для опасных. Не отключайте его через unconfined без документированного анализа syscall.
no-new-privileges запрещает процессу получать дополнительные privileges через execve, включая setuid/file capabilities.
6. Теория: AppArmor, SELinux и filesystem
Docker обычно применяет docker-default AppArmor либо SELinux labels. Denial ищут в kernel audit, а не обходят отключением LSM.
Read-only rootfs требует явных writable mounts для /tmp, cache, sockets и данных. noexec/nosuid уменьшают возможности временных каталогов.
7. Методические указания
- Начните с threat model и data classification
- Используйте non-root и numeric UID
- Drop ALL, затем add минимально
- Read-only rootfs плюс точечные tmpfs/volumes
- Запрещайте privilege escalation
- Не монтируйте docker.sock и host root
- Фиксируйте effective HostConfig
- Тестируйте негативные сценарии
8. Лабораторная работа
Сравните default и hardened containers по UID, CapEff, seccomp и HostConfig.
docker info --format '{{json .SecurityOptions}}' | jq
docker run --name docker-m13-default alpine:3.21 sh -c 'id; grep -E "^(CapEff|CapBnd|NoNewPrivs|Seccomp):" /proc/1/status; cat /proc/1/attr/current 2>/dev/null || true; touch /root/default-write'
docker inspect docker-m13-default --format '{{json .HostConfig}}' | jq '{Privileged,ReadonlyRootfs,CapAdd,CapDrop,SecurityOpt,NetworkMode}'docker run --name docker-m13-hardened --user 65532:65532 --read-only --tmpfs /tmp:rw,noexec,nosuid,size=16m --cap-drop ALL --security-opt no-new-privileges --pids-limit 64 --network none alpine:3.21 sh -c 'id; grep -E "^(CapEff|CapBnd|NoNewPrivs|Seccomp):" /proc/1/status; touch /tmp/allowed; if touch /etc/forbidden 2>/dev/null; then exit 1; else echo rootfs_read_only=enforced; fi; test ! -e /proc/net/route || cat /proc/net/route'
docker inspect docker-m13-hardened --format '{{json .HostConfig}}' | jq '{Privileged,ReadonlyRootfs,CapAdd,CapDrop,SecurityOpt,NetworkMode,PidsLimit}'
docker diff docker-m13-hardenedДокажите действие CHOWN и default seccomp без --privileged.
docker run --rm alpine:3.21 sh -c 'touch /tmp/item; chown 1000:1000 /tmp/item; stat -c "uid=%u gid=%g" /tmp/item'
docker run --rm --cap-drop ALL alpine:3.21 sh -c 'touch /tmp/item; if chown 1000:1000 /tmp/item 2>/dev/null; then exit 1; else echo chown_capability_removed; fi'
docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3.21 sh -c 'touch /tmp/item; chown 1000:1000 /tmp/item; stat -c "uid=%u gid=%g" /tmp/item'docker run --rm alpine:3.21 sh -c 'grep "^Seccomp:" /proc/1/status; if unshare --user true 2>/dev/null; then echo unexpected; else echo unshare_denied; fi'
docker inspect docker-m13-hardened --format 'apparmor={{.AppArmorProfile}} security={{json .HostConfig.SecurityOpt}}'Критерий: hardened UID non-root, CapEff=0, NoNewPrivs=1, rootfs read-only, network none; точечный CHOWN разрешает смену владельца.
9. Диагностика Permission denied
Отказ может дать DAC, capability, seccomp, LSM, read-only mount или device cgroup. Определите слой до изменения policy.
docker inspect docker-m13-hardened --format '{{json .State}}' | jq
docker inspect docker-m13-hardened --format '{{json .HostConfig}}' | jq
docker logs docker-m13-hardened
docker events --since 10m --until 0s --filter container=docker-m13-hardened
journalctl -k --since '10 minutes ago' --no-pager | grep -Ei 'apparmor|selinux|seccomp|denied' | tail -30 || true- id/stat — identity и mode
- CapEff/CapBnd — capability
- Seccomp и SecurityOpt — syscall policy
- AppArmorProfile/audit — LSM
- Mounts/ReadonlyRootfs — filesystem
- devices и namespace mode — exposure
10. Контроль и cleanup
- Почему container root не безопасен автоматически?
- Чем rootless отличается от --user?
- Почему docker.sock root-equivalent?
- Зачем drop ALL?
- Что делает seccomp?
- Что запрещает no-new-privileges?
- Как найти LSM denial?
- Почему --privileged не исправление?
docker rm docker-m13-default docker-m13-hardened
docker ps -a --filter name=docker-m13Модуль освоен, если вы выдаёте только доказанно нужные полномочия и локализуете denial по слоям.