Изоляция, сканирование уязвимостей, security best practices
Безопасность — критический аспект production-развёртывания. Изучите лучшие практики защиты контейнеров от уязвимостей и атак.
Основные риски:
Проблема: По умолчанию контейнеры запускаются от root.
# ❌ Плохо — запуск от root
FROM python:3.11-slim
COPY app.py .
CMD ["python", "app.py"]
# ✅ Хорошо — создание пользователя
FROM python:3.11-slim
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /app
COPY . .
USER appuser
CMD ["python", "app.py"]Почему важно:
services:
app:
image: myapp:latest
user: "1000:1000" # UID:GID
# или
user: appuser# Сканирование образа
docker scout cve myapp:latest
# Рекомендации
docker scout recommendations myapp:latest
# Сравнение версий
docker scout compare myapp:v1.0 myapp:v2.0# Установка
brew install trivy
# Сканирование образа
trivy image myapp:latest
# Сканирование с отчётом
trivy image --format table --output report.html myapp:latest
# Только критические уязвимости
trivy image --severity CRITICAL myapp:latest
# Сканирование Dockerfile
trivy config Dockerfile# Установка
npm install -g snyk
# Аутентификация
snyk auth
# Сканирование образа
snyk container test myapp:latest
# Сканирование с исправлениями
snyk container test myapp:latest --file=Dockerfile# GitHub Actions
- name: Scan for vulnerabilities
run: |
trivy image --exit-code 1 --severity CRITICAL myapp:latest# Секреты в Dockerfile
ENV DATABASE_PASSWORD=supersecret123
COPY .env /app/.env
# Хардкод в коде
PASSWORD = "admin123"# Секреты в docker-compose.yml
services:
app:
environment:
- DATABASE_PASSWORD=supersecret123
- API_KEY=sk-1234567890# Создание секрета
echo "supersecret123" | docker secret create db_password -
# Использование в сервисе
docker service create \
--secret db_password \
--name myapp \
myapp:latest# docker-compose.yml (Swarm mode)
version: '3.8'
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: true# Чтение секрета в приложении
with open('/run/secrets/db_password', 'r') as f:
password = f.read().strip()# .env (добавить в .gitignore)
DATABASE_PASSWORD=supersecret123
API_KEY=sk-1234567890# docker-compose.yml
services:
app:
image: myapp:latest
env_file:
- .env# HashiCorp Vault
services:
app:
image: myapp:latest
environment:
- VAULT_ADDR=http://vault:8200
# Секреты загружаются при старте через init-контейнер# Dockerfile
FROM python:3.11-slim
# Секрет доступен только во время сборки
RUN \
cp /run/secrets/pip_conf /etc/pip.conf && \
pip install -r requirements.txt
CMD ["python", "app.py"]# Сборка с секретом
docker build --secret id=pip_conf,src=~/.pip/pip.conf -t myapp .Проблема: Без ограничений контейнер может потребить все ресурсы хоста.
# Ограничение памяти
docker run -d \
--memory=512m \
--memory-swap=512m \
myapp:latest
# Ограничение CPU
docker run -d \
--cpus=1.5 \
--cpu-shares=512 \
myapp:latest
# Ограничение процессов
docker run -d \
--pids-limit=100 \
myapp:latest
# Комбинированное ограничение
docker run -d \
--memory=1g \
--cpus=2 \
--pids-limit=50 \
--read-only \
--tmpfs /tmp:size=100m \
myapp:latestservices:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1'
memory: 512M
pids: 100
reservations:
cpus: '0.5'
memory: 256Mdocker run -d \
--read-only \
--tmpfs /tmp:size=100m \
--tmpfs /var/run:size=10m \
myapp:latestПреимущества:
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:size=100m
- /var/run:size=10mversion: '3.8'
services:
frontend:
image: nginx
networks:
- frontend-network
api:
image: myapi
networks:
- frontend-network
- backend-network
db:
image: postgres
networks:
- backend-network
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
internal: true # Нет доступа наружуПринцип наименьших привилегий:
# ✅ Хорошо — только localhost
docker run -d -p 127.0.0.1:5432:5432 postgres
# ❌ Плохо — доступно всем интерфейсам
docker run -d -p 5432:5432 postgres# docker-compose.yml
services:
db:
image: postgres
ports:
- "127.0.0.1:5432:5432" # Только localhostapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-access
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432# Удалить все capabilities, добавить только необходимые
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
myapp:latestCommon capabilities:
NET_BIND_SERVICE — привязка к портам < 1024CHOWN — изменение владельца файловSETUID, SETGID — изменение UID/GIDSYS_CHROOT — использование chrootservices:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE# Проверка профиля
docker inspect --format='{{.AppArmorProfile}}' container_id
# Использование кастомного профиля
docker run -d \
--security-opt apparmor=/etc/apparmor.d/myapp \
myapp:latest# Запуск с SELinux
docker run -d \
--security-opt label=level:s0:c100,c200 \
myapp:latest
# Отключение SELinux для контейнера (не рекомендуется)
docker run -d \
--security-opt label=disable \
myapp:latestSeccomp ограничивает системные вызовы.
# Запуск с кастомным профилем
docker run -d \
--security-opt seccomp=/path/to/seccomp.json \
myapp:latest
# Отключение (не рекомендуется)
docker run -d \
--security-opt seccomp=unconfined \
myapp:latest{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["accept", "bind", "connect"],
"action": "SCMP_ACT_ALLOW"
}
]
}Проблема: Контейнер может работать, но приложение не отвечать.
HEALTHCHECK \
CMD curl -f http://localhost:8000/health || exit 1Проверка статуса:
docker ps # Покажет (healthy) или (unhealthy)
docker inspect --format='{{.State.Health.Status}}' container_idservices:
api:
image: myapi:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10sdocker run -d \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp:latest# docker-compose.yml
services:
app:
image: myapp:latest
logging:
driver: syslog
options:
syslog-address: "udp://logserver:514"
tag: "myapp"# Просмотр событий
docker events
# Фильтрация
docker events --filter 'type=container' --filter 'action=start'latest)--cap-drop=ALL)internal: true для backend-сетейПодпись образов:
# Включить DCT
export DOCKER_CONTENT_TRUST=1
# Подписать образ при push
docker push myrepo/myapp:v1.0
# При pull проверяется подпись
docker pull myrepo/myapp:v1.0# Многоэтапная сборка
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production этап
FROM node:18-alpine
# Метаданные
LABEL maintainer="security@example.com"
# Установка зависимостей для health check
RUN apk --no-cache add curl
# Создание пользователя
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Копирование артефактов
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./
# Переключение на непривилегированного пользователя
USER appuser
# Health check
HEALTHCHECK \
CMD curl -f http://localhost:3000/health || exit 1
# Порт
EXPOSE 3000
CMD ["node", "dist/index.js"]# Проверить, куда пытается писать приложение
docker logs container_id
# Добавить tmpfs для записи
docker run -d --read-only --tmpfs /tmp:size=100m myapp# Добавить только необходимую capability
docker run -d --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Или использовать setuid бинарник (не рекомендуется)# Обновить базовый образ
docker pull python:3.11-slim
# Пересобрать образ
docker build --no-cache -t myapp:latest .
# Проверить снова
trivy image myapp:latest--read-only с tmpfs для записи--cap-drop=ALL, добавлять только нужныеКонтекст: FinTech стартап с API на Python (FastAPI).
Инцидент:
urllib3python:3.9 (не обновлялся 18 месяцев)Причина:
# ❌ Было — образ 2021 года
FROM python:3.9.0
# urllib3==1.26.3 с уязвимостьюРешение:
# ✅ Стало — автоматическое обновление
FROM python:3.11-slim
# Сканирование в CI/CD
# RUN trivy image --exit-code 1 --severity CRITICAL .
# Обновление зависимостей
RUN apt update && apt upgrade -y && rm -rf /var/lib/apt/lists/*
# Установка зависимостей с проверкой версий
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Проверка уязвимостей
# RUN pip-audit || trueПроцесс безопасности:
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * *' # Ежедневно в 6:00
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image \
--exit-code 1 \
--severity CRITICAL,HIGH \
myapp:${{ github.sha }}
- name: Upload report
if: failure()
uses: actions/upload-artifact@v3
with:
name: trivy-report
path: trivy-report.sarifРезультат:
| Метрика | До | После | Улучшение |
|---|---|---|---|
| Критические уязвимости | 23 | 0 | 100% |
| Время обнаружения | Недели | <24 часа | 7x |
| Возраст базового образа | 18 мес | <1 мес | 18x |
Выводы:
Контекст: E-commerce платформа с Docker Compose.
Инцидент:
.env с паролями в публичный репозиторийПричина:
# ❌ Было — секреты в репозитории
cat docker-compose.yml
# environment:
# - DATABASE_PASSWORD=SuperSecret123 # Виден всем!
cat .env # Закоммичен в git!
# DATABASE_PASSWORD=SuperSecret123
# API_KEY=sk-1234567890Решение:
# ✅ Стало — Docker Secrets + external manager
# 1. Создание секрета
echo "SuperSecret123" | docker secret create db_password -
# 2. Использование в Compose
cat docker-compose.ymlversion: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
app:
image: myapp:latest
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: true# Чтение секрета в приложении
def get_secret(name):
with open(f'/run/secrets/{name}', 'r') as f:
return f.read().strip()
DB_PASSWORD = get_secret('db_password')
API_KEY = get_secret('api_key').gitignore для защиты:
# Никогда не коммитить!
.env
.env.*.local
*.pem
*.key
secrets/
credentials/Результат:
| Метрика | До | После | Улучшение |
|---|---|---|---|
| Секреты в git | 15 | 0 | 100% |
| Доступ к secrets | Все разработчики | Только runtime | ✅ |
| Audit log | Нет | Есть (Docker) | ✅ |
Контекст: Multi-tenant платформа с общим Docker-хостом.
Инцидент:
--privileged/var/run/docker.sockПричина:
# ❌ Было — privileged режим
docker run -d --privileged myapp
# Внутри контейнера:
mount -t proc none /mnt # Доступ к хосту!
cat /mnt/1/etc/shadow # Чтение паролей хостаРешение:
# ✅ Стало — минимальные capabilities
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--read-only \
--tmpfs /tmp:size=100m \
--security-opt seccomp=default \
--security-opt apparmor=docker-default \
myapp
# Блокировка доступа к docker.sock
# Никогда не монтировать /var/run/docker.sock в приложение!# docker-compose.yml с безопасностью
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:size=100m
- /var/run:size=10m
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
# Никогда не использовать:
# privileged: true # ❌
# devices: # ❌
# - /dev/sda:/dev/sdaHardening хоста:
# Запрет privileged контейнеров
# /etc/docker/daemon.json
{
"no-new-privileges": true,
"userns-remap": "default"
}
# Аудит контейнеров
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -v "alpine\|slim"
# Проверка на privileged
for container in $(docker ps -q); do
docker inspect $container --format='{{.HostConfig.Privileged}}' | grep -q true && \
echo "WARNING: $container is privileged!"
doneРезультат:
| Метрика | До | После | Улучшение |
|---|---|---|---|
| Privileged контейнеры | 12 | 0 | 100% |
| Доступ к docker.sock | 5 сервисов | 0 | 100% |
| Surface для атаки | Высокий | Минимальный | ✅ |
В следующей теме вы изучите работу с реестрами образов: Docker Hub, private registry, push и pull.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.