
Привет, Кореша! Вы когда-нибудь задумывались о безопасности контейнеров, работающих в продакшене? Docker и Kubernetes предоставляют широкий набор инструментов, которые могут быть использованы плохими людьми. Безопасность контейнеров — это не просто волшебная защита, а многослойная система, охватывающая весь процесс от сборки до запуска в кластере.
В этой статье мы разберем практические шаги по защите ваших контейнеров, от написания безопасного Dockerfile до настройки политик безопасности в Kubernetes.
Этап 1: Безопасность на этапе сборки (Dockerfile)
Безопасность начинается с образа. Ваша цель — создать минимальный, неизменяемый и безопасный артефакт.
1. Используйте минимальные базовые образы
Чем меньше пакетов внутри, тем меньше шанс для атаки.
Нельзя:
dockerfile
FROM ubuntu:latest  # Полноценная ОС со всеми утилитами и уязвимостями
RUN apt update && apt install -y python3
COPY app.py .
CMD ["python3", "app.py"]Можно:
dockerfile
FROM python:3.11-slim-bookworm  # Урезанный официальный образ на основе Debian slim
COPY app.py .
CMD ["python3", "app.py"]Идеально (для Go, Java, Node.js):
dockerfile
# Multi-stage build - идеальный вариант
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# Финальный образ на основе "scratch" (абсолютно пустой) или distroless
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /
CMD ["/myapp"]Образ distroless от Google содержит только ваше приложение и его минимальные зависимости, без shell, package manager и других стандартных утилит. Взломщику просто нечем будет воспользоваться.
2. Не запускайте процессы от root
Это золотое правило контейнерной безопасности.
Плохо:
dockerfile
FROM node:20
COPY . .
RUN npm install
CMD ["node", "index.js"] # Запускается от root!Хорошо:
dockerfile
FROM node:20-slim
# Создаем непривилегированного пользователя и группу
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Меняем владельца файлов приложения
COPY --chown=appuser:appgroup . /app
WORKDIR /app
RUN npm install
# Переключаемся на непривилегированного пользователя
USER appuser
CMD ["node", "index.js"]3. Сканируйте образы на уязвимости
Интегрируйте сканирование в CI/CD пайплайн.
bash
# Пример с Trivy (бесплатный и открытый сканер)
trivy image my-app:latest
# Или с Docker Scout
docker scout quickview my-app:latestЭтап 2: Безопасность на этапе выполнения (Kubernetes Security Contexts)
Kubernetes предоставляет мощный инструмент — securityContext, который позволяет fine-grained настройку прав контейнера.
1. Запрещаем запуск от root
Даже если в образе забыли указать USER.
yaml
apiVersion: v1
kind: Pod
metadata:
  name: security-demo
spec:
  containers:
  - name: sec-ctx-demo
    image: node:20-slim
    securityContext:
      runAsNonRoot: true # Kubernetes не даст запустить pod, если он работает от root
      runAsUser: 1000 # Явно указываем UID
      runAsGroup: 3000 # Явно указываем GID2. Ограничиваем возможности (Capabilities)
По умолчанию контейнер получает множество ненужных привилегий. Отзовите всё и дайте только необходимое.
yaml
securityContext:
  capabilities:
    drop: ["ALL"] # Отзываем ВСЕ возможности
    add: ["NET_BIND_SERVICE"] # Добавляем только одну: возможность занять порт ниже 10243. Запрещаем эскалацию привилегий
Важная настройка, которая не позволяет процессу внутри контейнера получить больше прав, чем у него есть.
yaml
securityContext:
  allowPrivilegeEscalation: false4. Режим только для чтения (ReadOnlyRootFilesystem)
Идеальная практика для неизменяемых контейнеров. Если вашему приложению не нужно ничего писать на диск — включайте.
yaml
securityContext:
  readOnlyRootFilesystem: true
# Но если нужно писать логи во временную папку
volumeMounts:
- name: temp-vol
  mountPath: /tmp
  readOnly: false
volumes:
- name: temp-vol
  emptyDir: {}Этап 3: Системные политики (Pod Security Standards)
SecurityContext — это хорошо для одного пода. Но что если нужно применить политики ко всему кластеру? Здесь на помощь приходят Pod Security Admission (встроено в K8s с v1.25+) или более старый PodSecurityPolicy (устарел).
Уровни политик PSA:
- Privileged: Без ограничений, для системных workloads. 
- Baseline: Минимальные ограничения, блокирующие известные эскалации привилегий. 
- Restricted: Жесткие ограничения, следующие best practices. 
Пример Namespace с политикой Restricted:
yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.29Теперь при попытке создать под без securityContext в этом неймспейсе он будет отвергнут.
Этап 4: Следующий уровень — AppArmor и seccomp
Для параноиков и тех, кому нужна максимальная защита
Seccomp — ограничивает системные вызовы, которые может делать процесс.
yaml
securityContext:
  seccompProfile:
    type: RuntimeDefault # Используем профиль по умолчанию, предоставленный container runtimeМожно создавать и свои кастомные профили, чтобы запрещать опасные вызовы вроде execve.
Практический чеклист для внедрения
- Сборка: Используйте multi-stage сборку и - distroless/- scratchобразы.
- Запуск: Всегда запускайте контейнеры от непривилегированного пользователя ( - USERв Dockerfile +- runAsNonRoot: trueв K8s).
- Политики: Настройте - Pod Security Admissionна уровне неймспейсов как минимум с- baselineуровнем.
- Сканирование: Встройте сканирование образов на уязвимости (Trivy) в CI/CD. 
- Сетевые политики: Не забывайте про - NetworkPolicyдля изоляции трафика между подами.
Заключение
Безопасность контейнеров — это не продукт, а процесс и правильная конфигурация. Начиная с минимального образа и заканчивая строгими политиками в кластере, вы выстраивайте многоуровневую защиту, которая серьезно усложнит жизнь злоумышленнику.
Если нужен шаблон правильных контейнеров, конфиг PSS, загляните на bfd cards, где эти вопросы разбираются регулярно.
А какие практики безопасности используете вы? Делитесь в комментариях!
Комментарии (8)
 - vsnikolaev06.09.2025 18:47- Спасибо за статью. У меня практический вопрос: я запускаю контейнер не от root, и вывожу файлы логов наружу. А в основной системе ("снаружи") владелец файла указан совсем странный. Я так понимаю это из-за совпадения UID/GID.Как их правильно размапить?  - chelaxe06.09.2025 18:47- Явно их указать: - ENV UID=1000 \ GID=1000 RUN addgroup --gid $GID appgroup && \ adduser --disabled-password --gecos "" --home /app --ingroup appgroup --no-create-home --uid $UID appuser- потом в compose - environment: UID: ${USER_UID:-1000} GID: ${USER_GID:-1000}- потом в env - USER_UID=1000 USER_GID=1000 - grinsv06.09.2025 18:47- Если не ошибаюсь, можно даже не создавать пользователя, просто в compose указать UID и GID. Примерно так: - services: app: environment: XDG_CACHE_HOME: /tmp/cache XDG_CONFIG_HOME: /tmp/config user: "${USER_UID:-1000}:${USER_GID:-1000}"
 
 
 - grinsv06.09.2025 18:47- Мне кажется, тема scratch-контейнеров немного не до конца раскрыта. Возможно, следует дописать отдельную версию докерфайла, или отдельный стейдж в том же докерфайле. 
 
           
 

Tony-Sol
В статье go, node и python.
Как так то?
apelsin1998 Автор
спешил, перепутал с другой статьей