Чому безпека контейнерів не може бути запізнілою думкою
Контейнери забезпечують ізоляцію, але за замовчуванням вони не є межею безпеки. Неправильно сконфігурований контейнер може відкрити файлові системи хоста, витекти облікові дані або надати атакуючому плацдарм для горизонтального переміщення вашою інфраструктурою. Модель спільного ядра означає, що вихід з контейнера зачіпає кожне навантаження на хості.
У DevOpsVibe ми регулярно проводимо аудит контейнерних середовищ. Ось десять практик, які роблять найбільшу різницю між вразливим розгортанням і зміцненим.
1. Використовуйте мінімальні базові образи
Кожен пакет у вашому образі — це потенційна поверхня атаки. Починайте з найменшого образу, який працює:
До (вразливий):
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip curl wget vim
COPY . /app
RUN pip3 install -r /app/requirements.txt
CMD ["python3", "/app/main.py"]
Після (зміцнений):
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["main.py"]
Distroless-образ не має shell, пакетного менеджера та непотрібних утиліт. Якщо атакуючий отримає виконання коду, працювати буде майже ні з чим.
Порівняння розмірів образів:
ubuntu:22.04із залежностями: ~450MBpython:3.12-slimmulti-stage з distroless: ~85MB
2. Ніколи не запускайте від root
За замовчуванням Docker запускає процеси як root всередині контейнера. Це небезпечно, бо вихід з контейнера дає атакуючому root на хості:
FROM node:20-slim
# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN npm ci --only=production
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
Перевірте під час виконання:
docker run --rm my-app whoami
# Output: appuser
3. Скануйте образи на вразливості
Інтегруйте сканування образів у CI/CD-конвеєр. Ми рекомендуємо Trivy за швидкість та вичерпну базу даних:
# Scan during build
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest
# Example output:
# my-app:latest (debian 12.4)
# Total: 0 (HIGH: 0, CRITICAL: 0)
Автоматизуйте це у вашому конвеєрі:
# .github/workflows/security.yml
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: "1"
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Також скануйте свої Dockerfile на неправильні налаштування:
trivy config --severity HIGH,CRITICAL ./Dockerfile
4. Закріплюйте digest образів, а не лише теги
Теги змінні. Хтось може запушити скомпрометований образ у python:3.12-slim, і кожна збірка, що тягне цей тег, отримує шкідливу версію:
# Risky: tag can be overwritten
FROM python:3.12-slim
# Secure: digest is immutable
FROM python:3.12-slim@sha256:a3e58c29e4a3b692a57d0e68b4b9c4f2e63e547e1b3f0a8e93c2a4fb7d3e1c9a
Отримайте digest за допомогою:
docker inspect --format='{{index .RepoDigests 0}}' python:3.12-slim
5. Впроваджуйте read-only файлові системи
Запобігайте зміні файлової системи контейнера під час виконання. Якщо ваш застосунок повинен писати, монтуйте конкретні writable-томи:
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--volume /app/data:/data:rw \
my-app:latest
У Kubernetes:
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
6. Правильно керуйте секретами
Ніколи не запікайте секрети в образи. Вони залишаються в історії шарів, навіть якщо ви видаляєте їх у пізнішому шарі:
Неправильно:
# NEVER do this
ENV DATABASE_URL=postgres://admin:password@db:5432/myapp
COPY .env /app/.env
Правильно — використовуйте секрети під час збірки (BuildKit):
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=db_url \
export DATABASE_URL=$(cat /run/secrets/db_url) && \
python manage.py collectstatic
docker buildx build --secret id=db_url,src=./db_url.txt -t my-app .
Під час виконання використовуйте управління секретами вашого оркестратора:
# Kubernetes secret injection
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
7. Обмежуйте ресурси контейнера
Необмежений контейнер може спожити всі ресурси хоста, створивши умови для відмови в обслуговуванні:
docker run \
--memory=512m \
--memory-swap=512m \
--cpus=1.0 \
--pids-limit=256 \
--ulimit nofile=1024:1024 \
my-app:latest
У Kubernetes завжди встановлюйте requests і limits ресурсів:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
8. Скидайте всі capabilities
Linux capabilities дають точний контроль над тим, що може робити процес. Скиньте їх усі та додайте назад лише те, що вам потрібно:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app:latest
Поширені capabilities і коли вони вам справді потрібні:
| Capability | Випадок використання |
|---|---|
NET_BIND_SERVICE | Прив'язка до портів нижче 1024 |
CHOWN | Зміна власника файлу (рідко потрібна) |
SYS_PTRACE | Лише для дебагу, ніколи в продакшні |
SYS_ADMIN | Майже ніколи -- це майже еквівалентно root |
9. Використовуйте multi-stage збірки, щоб виключити інструменти збірки
Залежності збірки, як-от компілятори, дебагери та вихідний код, ніколи не повинні з'являтися у вашому продакшн-образі:
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Stage 2: Production
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
Кінцевий образ містить лише скомпільований бінарник і TLS-сертифікати. Жодного Go toolchain, жодного вихідного коду, жодного shell.
10. Увімкніть Docker Content Trust і підписуйте образи
Переконайтеся, що у вашому середовищі запускаються лише перевірені образи:
# Enable content trust
export DOCKER_CONTENT_TRUST=1
# Sign and push
docker trust sign myregistry.com/my-app:v1.2.3
# Verify
docker trust inspect myregistry.com/my-app:v1.2.3
Для Kubernetes-середовищ використовуйте admission controllers, як-от Kyverno або OPA Gatekeeper, щоб забезпечувати політики образів:
# Kyverno policy: require signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
Чеклист безпеки
Перед розгортанням будь-якого контейнера в продакшн перевірте:
- Базовий образ мінімальний (distroless, Alpine або scratch)
- Образ просканований на CVE з нульовими знахідками HIGH/CRITICAL
- Контейнер запускається як non-root користувач
- Файлова система read-only, де це можливо
- Жодних секретів, запечених в образ
- Встановлені ліміти ресурсів
- Усі непотрібні capabilities скинуті
- Теги образів закріплені до digest
- Образи підписані та перевірені
- Network policies обмежують комунікацію контейнерів
Висновок
Безпека контейнерів — це не один інструмент чи одноразовий аудит. Це набір практик, вбудованих у кожен етап вашого конвеєра — від Dockerfile до runtime-конфігурації. Десять практик вище адресують найпоширеніші вектори атаки, які ми бачимо в продакшн-середовищах.
У DevOpsVibe ми допомагаємо командам будувати безпечні контейнерні конвеєри з нуля. Чи потрібен вам аудит безпеки існуючого налаштування, чи ви хочете впровадити DevSecOps-практики у вашій організації, у нашої команди є експертиза, щоб зробити це правильно. Зв'яжіться з нами, щоб розпочати розмову.