В этой статье представлен список инструментов Linux и функционала Kubernetes, регулирующих безопасность контейнеров. Статья имеет описательный характер для базового понимания настроек безопасности, а также для систематизации полезных инструментов и полей спецификации k8s для этих целей. Статья основана на содержании книг, представленных в главе «Литература».
Безопасность контейнеров в Linux
Основной особенностью контейнеров является изоляция процессов средствами Linux. При этом, приложения в контейнере делят общее ядро операционной системы с другими приложениями. Какие же ограничения применяют контейнеры?
Контейнеры должны контролировать права процесса, доступ к ресурсам хоста и доступ к другим файлам файловой системы. Разберемся со всем по порядку.
Ограничения прав процесса
Можно настроить очень простое разграничение доступа для процесса: наличие или отсутствие прав root.
Также можно разрешить запускать некоторые процессы с привилегиями root обычному пользователю, повесив файлу флаг Setuid (если владелец файла - пользователь root). Setuid позволяет вызвать процесс от имени владельца, а не от имени запускающего пользователя.
Обычно флаг setuid (+s) используют вспомогательные программы для назначения capabilities - возможностей, для которых требуются повышенные привилегии.
Это небезопасно, например, можно задать бит +s для bash-скрипта и через него под привилегиями root выполнить любое содержимое этого скрипта. Но это более правильный вариант, чем давать исполняемому файлу полные права админа.
Лучшим решением является задать исполняемому файлу нужные capabilities вручную под пользователем root и запускать его с ограниченным набором привилегированных возможностей (capabilities) от стандартного пользователя.
Контроль ресурсов
В Linux контроль ресурсов происходит благодаря cgroups (control groups). Инструмент cgroups помогает контролировать ресурсы cpu, памяти, сетевого трафика для групп процессов. Этот инструмент также помогает контролировать количество процессов, чтобы избежать многократного увеличения их числа.
Можно создать новую cgroup чтобы:
контролировать конкретный параметр конкретного процесса;
изменить параметры уже созданного по унаследованному принципу набора cgroup для запущенного процесса.
Если мы зададим некорректные параметры в cgroup (недостаточные объемы памяти для процесса, например) то процесс при превышении лимитов будет уничтожен.
Контроль файловой системы
Chroot задает зону видимости для процесса и его дочерних процессов, считая текущую папку корнем системы. Это является оптимальным решением для контейнеров. При использовании chroot, если требуемые зависимости и библиотеки не входят в установленную зону видимости, они не смогут быть выполнены в основном процессе.
Слабое место Docker daemon
Демон docker с целью создания нового пространства имен для контейрера должен запускаться с привилегиями пользователя root. В связи с этим, команда
docker build <…> && docker run <…> позволяют выполнить любую команду.
В качестве альтернативы docker daemon можно использовать, к примеру, инструменты buildkitили kaniko.
Хранение чувствительных данных
Чувствительные данные, зашитые внутрь образа можно просмотреть в истории архива образа даже после удаления файла с описанием образа. Соответственно, хранить чувствительную информацию, вшитую в любую часть приложения, напрямую - абсолютно не безопасно.
Как обезопасить Dockerfile:
Использовать базовый образ с минимальным количеством библиотек.
В процессе сборки образа удалять код, который не используется после сборки контейнера.
Запрещать запуск процессов от имени root в Dockerfile (подход rootless-container).
Ограничить доступ к модификации Dockerfile, так как команда RUN может вызывать даже удаленно-исполняемый код.
Монтировать только временные директории ОС, и никогда не монтировать директории файловой системы такие, как: /etc/, /bin/ и т.д.
Не хранить чувствительные данные в Dockerfile.
Не использовать файлы с разрешением setuid bit.
Не использовать лишний код, вшитый в ОС, использовать scratch image - образы с минимальным количеством предустановленных библиотек.
Не скачивать пакеты в моменте выполнения процесса, а хранить заранее загруженные пакеты рядом с файлом описания образа.
Важной и оптимальной настройкой является невозможность изменять текущие контейнеры во время их работы (Immutable containers). В таком случае, для внесения изменений необходимо запустить новый контейнер, основанный на образе с внесенными изменениями, а старые работающие контейнеры удалить).
Чтобы понизить привилегии пользователя контейнера (запускать контейнер не с правами root и на хосте и внутри контейнера) можно поменять uid пользователя (в config.json или в директиве USER в Dockerfile) на любой желаемый (например, 5000).
В docker можно напрямую указывать uid пользователя (например, --user 5000) при запуске контейнера.
Запускать контейнер только с необходимыми ему привилегиями (capabilities).
Инструменты повышения безопасности контейнеров
Рассмотрим разные способы изоляции системных вызовов к ядру для контейнеров. Лучшим решением является настройка совокупности инструментов защиты для формирования «рубежей защиты» контейнеров.
AppArmor - инструмент упреждающей защиты, основанный на политиках безопасности, которые определяют, к каким системным ресурсам и с какими привилегиями может получить доступ то или иное приложение. Его можно рассматривать как либо белый, либо черный список системных вызовов для процесса.
SElinux - мандатная система разграничения доступа в дополнение к дискреционной (DAC).
Gvisor от Google - песочница, позволяющая работать с контейнерами, обеспечивая сходный с полноценной виртуализацией уровень безопасности. Недостаток: запускает ограниченное число системных вызовов к ядру (ограничивает работу 97 системных вызовов против ограничения 44 системных вызовов docker daemon). Преимущество: Gvisor скрывает запускаемые процессы внутри контейнера при просмотре запущенных процессов хоста.
Kata containers - инструмент, который создает прокси между контейнером и хостом; прокси, в свою очередь, создает VM на QEMU для запуска контейнера от имени этой VM.
Firecracker - очень легковесная VM (эмулирует только 4 устройства ОС) на KVM, которая запускает runtime за 100 мс.
Unikernels - образ, который компилирует ядро и необходимые библиотеки с исполняемым кодом в отдельный бинарный файл и запускает его как отдельную ОС.
ServiceMesh и mTLS - использование сертификатов для двусторонней аутентификации в sidecar-container (для Kubernetes).
Необходимо также реализовывать инфраструктуру для отзыва сертификата при его компрометации (certificate revocation).
Настройки безопасности контейнеров в Kubernetes
Здесь описаны основные настройки спецификации k8s, влияющие на безопасность контейнеров и модулей.
pod.spec.hostNetwork = true - позволяет модулю использовать сетевые адаптеры узла вместо собственных виртуальных сетевых адаптеров. Когда компонент плоскости управления Kubernetes разворачивается как модуль, он использует параметр hostNetwork.
spec.containers.port.hostPort = true - позволяет модулям привязываться к порту в стандартном пространстве имён узла, но при этом иметь собственное сетевое пространство имён. hostPort перенаправляет подключение непосредственно в модуль (pod) этого узла (в противовес этому, стандартная служба k8s nodePort перенаправляет трафик случайно выбранному модулю с любого узла). Соответственно, на узле может быть только один модуль, имеющий в спецификации привязку к этому порту. Настройка hostPort говорит о том, что порт узла привязан к тем узлам, на которых могут выполняться модули с этой функцией, а служба nodePort привязывает порт модуля к порту хоста на любом узле.
Каждый модуль имеет свое дерево процессов, так как он имеет свое собственное пространство имён для PID и использует собственное пространство имён IPC, позволяя через механизм межпроцессорного взаимодействия (IPC) взаимодействовать друг с другом только процессам, которые находятся в одном модуле.
spec.containers.hostPID = true и
spec.containers.hostIPC = true - позволяют видеть другие процессы на узле и взаимодействовать с ними через межпроцессорное взаимодействие. По принципу работы аналогичны hostNetwork.spec.containers.securityContext.readOnlyRootFilesystem = true - не допускает записи контейнером в свою файловую систему, где хранится исполняемый код приложения. Это позволяет предотвратить внедрение вредоносного кода в контейнер. Вместо этого можно записывать информацию в примонтированный том.
spec.securityContext.fsGroup. Если два контейнера в одном модуле запущены от двух разных пользователей, то они не смогут читать и записывать файлы друг друга через примонтированный том. Для этого можно указывать дополнительные группы (fsGroup), работающие в контейнерах, чтобы обмениваться файлами независимо от id пользователя.
spec.securityContext.fsGroupChangePolicy - возможность изменения владельца и разрешений для примонтированного тома.
spec.securityContext.runAsUser - запуск от определенного идентификатора пользователя.
spec.securityContext.runAsGroup - запуск от пользователя, включенного в группу с определенным идентификатором.
spec.securityContext.SELinux - разграничение доступа посредством SELinux.
spec.securityContext.AppArmor - запрет определенных привилегированных возможностей (capabilities) посредством AppArmor.
spec.securityContext.allowPrivilegeEscalation - возможность получения более высоких привилегий в сравнении с родительским процессом.
Листинг 2
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
hostIPC: false # Контейнерам не разрешается
hostPID: false # использовать пространства
hostNetwork: false # имен хоста IPC, PID или Network
hostPorts: # Они могут привязываться только к
– min: 10000 # портам хоста от 10 000 до 11 000
max: 11000 # (включительно) или портам хоста
– min: 13000 # от 13 000 до 14 000
max: 14000
privileged: false # Контейнеры не могут работать в привилегированном режиме
readOnlyRootFilesystem: true # Контейнеры принудительно запускаются с корневой файловой системой, доступной только для чтения
runAsUser:
rule: RunAsAny # Контейнеры могут работать от имени любого пользователя и любой группы
fsGroup:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
seLinux:
rule: RunAsAny # Они также могут использовать любые группы SELinux, которые они хотят
volumes:
– '*' # В модулях можно использовать все типы томов
Некоторые из перечисленных параметров могут применяться не только на уровне контейнера, но и на уровне пода (e.g. pod.spec.securityContext). При этом на уровне контейнера эти настройки можно переопределить.
CAPABILITIES в Kubernetes
Листниг настройки capabilities:
apiVersion: v1
kind: Pod
metadata:
name: pod-add-settime-capability
spec:
containers:
– name: main
image: alpine
command: ["/bin/sleep", "999999"]
allowedCapabilities:
– SYS_TIME # Позволяет контейнерам добавлять capability SYS_TIME
defaultAddCapabilities:
– CHOWN # Автоматически добавляет в каждый контейнер capability CHOWN
requiredDropCapabilities: # Требует от контейнеров игнорировать capability SYS_ADMIN и SYS_MODULE
– SYS_ADMIN
– SYS_MODULE
securityContext:
capabilities:
add: # добавляет capabilities CAP_SYS_TIME
– SYS_TIME
Функциональные возможности ядра Linux обозначаются префиксом CAP_. В секции spec префикс CAP_ нужно исключать.
Capability CHOWN позволяет процессам менять владельца файла, включена по умолчанию. Включенные по умолчанию возможности можно отключать.
Pod Security Admission (PSA)
Pod Security Admission регулирует безопасность модулей. PSA устанавливает уровни безопасности модулей (Pod Security levels) для заданного режима Pod Security Admission (Pod Security Admission Mode) в конкретном пространстве имен.
Таким образом, мы устанавливаем степень защищенности модуля, выбирая для него уровень безопасности (насколько сильно требуется ограничить возможности модуля) и режим безопасности (что произойдет при нарушении политики).
PSA = Pod Security levels x Mode of Pod Security Admission.
Рассмотрим алгоритм работы PSA чуть подробнее.
Уровни безопасности для модулей (Pod Security levels)
Существует три уровня безопасности для модулей: privileged, baseline и restricted; они содержат совокупность стандартов безопасности для изоляции и ограничения поведения модулей. Ограничения применяются на уровне пространства имен при создании модуля. Kubernetes предлагает встроенный контроллер Pod Security Admission для обеспечения соблюдения этих стандартов.
Уровень безопасности |
Описание |
Privileged |
Неограниченная политика, обеспечивающая максимально возможный уровень разрешений. Эта политика допускает повышение привилегий. |
Baseline |
Минимально ограничивающая политика, которая предотвращает повышение привилегий. Разрешает конфигурацию модуля по умолчанию (минимально заданную). |
Restricted |
Политика с жесткими ограничениями в соответствии с текущими рекомендациями по ужесточению требований к модулю. |
Режимы Pod Security Admission (Mode)
Kubernetes определяет набор меток, которые можно установить, чтобы определить, какой из предопределенных стандартных уровней безопасности модулей необходимо использовать для конкретного пространства имен. Метки описывают три режима контроля: enforce, audit, warn.
Режим |
Описание |
enforce |
Нарушения политики приведут к отклонению запуска модуля. |
audit |
Нарушения политики приведут к добавлению примечания аудита к событию, зарегистрированному в журнале аудита, но в остальном действия разрешены. |
warn |
Нарушения политики приведут к появлению предупреждения для пользователя, но в остальном действия разрешены. |
Листинг PSA для конкретного пространства имен
apiVersion: v1
kind: Namespace
metadata:
name: my-baseline-namespace
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.28
Network Policies (NP)
Для ограничения взаимодействия объектов внутри кластера Kubernetes применяются сетевые политики. Они могут регулировать взаимодействие, формируя белые списки для трех видов объектов:
Другие разрешенные модули - pod, (исключение: модуль не может блокировать доступ к самому себе).
Разрешенные пространства имен - namespace.
Блоки IP-адресов - CIDR, (исключение: трафик к узлу, на котором запущен модуль, и с него разрешен всегда, независимо от IP-адреса модуля или узла).
Для определения сетевой политики необходимо указать, для какого типа трафика используется белый список подключений:
Ingress - политика применяется для входящего трафика.
Egress - политика применяется для исходящего трафика.
Также необходимо указать порты, по которым объектам разрешено взаимодействовать.
При определении сетевой политики на основе модулей (pod) или пространства имен (namespace) используется селектор меток, чтобы указать, какой входящий и исходящий трафик разрешен для модулей.
Когда создаются сетевые политики на основе IP-адресов, они определяются с помощью блоков IP-адресов (диапазонов CIDR).
Сетевые политики (Network policy) реализуются с помощью сетевого плагина (CNI). Без Container Network Interface настроить сетевые политики невозможно.
Листинг Network Policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
Заключение
Статья является теоретическим описанием наиболее значимых инструментов и механизмов обеспечения безопасности в контейнерах и среде Kubernetes.
Для формирования безопасной среды выполнения процессов в контейнерах необходимо совмещать параметры безопасности как уровня контейнера, так и уровня Kubernetes.
Совокупность методов защиты контейнера многократно повышает сложность реализации атак на приложение и снижает возможность эскалации привилегий, которая достигается простыми методами в контейнере без дополнительных настроек защиты.
Спасибо за прочтение!
Литература
«Kubernetes в действии» - Марко Лукша
«Container Security» - Liz Rice