core code helper

Безпека Docker-образів: non-root, мінімальні базові образи і сканування в CI

Практичний мінімум безпеки контейнерів: від вибору базового образу до сканування вразливостей, підпису образів і обмежень під час запуску.

0 імпортували Імпортувати собі

Контейнер — не віртуальна машина. Він ділить ядро з хостом, і погано зібраний образ, запущений з зайвими правами, — короткий шлях від вразливості в бібліотеці до компрометації сервера. Хороша новина: більшість ризиків закривається кількома простими звичками.

1. Мінімальний базовий образ

Кожен пакет в образі — потенційна вразливість. Порівняння типових варіантів:

Базовий образ Розмір Що всередині
ubuntu:24.04 ~80 МБ Повна ОС, shell, apt
python:3.13 ~1 ГБ Debian + компілятори
python:3.13-slim ~130 МБ Мінімальний Debian
alpine ~8 МБ musl, busybox
distroless / chainguard 2–50 МБ Лише рантайм, без shell і пакетного менеджера

Для компільованих мов (Go, Rust) — distroless/static або навіть scratch. Для Python і Node — -slim або distroless з multi-stage збіркою.

Порада

Відсутність shell в образі — це плюс до безпеки: зловмиснику, який отримав виконання коду, набагато складніше щось робити далі. Для налагодження використовуйте kubectl debug з окремим образом-інструментом.

2. Не root

FROM python:3.13-slim
RUN useradd --create-home --uid 10001 app
WORKDIR /home/app
COPY --chown=app:app . .
USER 10001

Числовий UID (а не ім'я) дозволяє Kubernetes перевірити runAsNonRoot без читання /etc/passwd.

3. Секрети не в образі

  • жодних .env, ключів і ARG PASSWORD=... в образі — шари зберігають усе;
  • .dockerignore з .env*, *.pem, .git;
  • секрети під час збірки — через RUN --mount=type=secret;
  • секрети під час роботи — зі сховища секретів (Vault, AWS Secrets Manager) або змінних середовища оркестратора.
# Перевірити, що в образі немає секретів
trivy image --scanners secret myapp:1.4.2

4. Сканування вразливостей у CI

# фрагмент GitHub Actions
- name: Сканування образу
  # у реальному пайплайні фіксуйте action за SHA коміту, а не за тегом
  uses: aquasecurity/trivy-action@v0.36.0
  with:
    image-ref: ghcr.io/acme/api:${{ github.sha }}
    severity: CRITICAL,HIGH
    ignore-unfixed: true      # не блокувати на вразливостях без виправлення
    exit-code: "1"
  • ignore-unfixed — інакше пайплайн стоятиме через CVE, для яких ще немає патча;
  • винятки — у файлі .trivyignore з коментарем, чому вразливість не застосовна і до якої дати переглянути;
  • скануйте не лише при збірці, а й за розкладом: нові CVE з'являються для вже задеплоєних образів.
Ланцюжок постачання — теж ваша поверхня атаки

У березні 2026 року зловмисники перезаписали десятки тегів версій самого trivy-action на шкідливий код (advisory GHSA-69fq-xp46-6x23). Пайплайни, що посилались на тег, запускали чужий код із доступом до секретів CI. Фіксуйте сторонні actions за SHA коміту і переглядайте оновлення як звичайні зміни коду.

Увага

Сотня «HIGH» у звіті, на які всі давно не дивляться, — гірше, ніж десять, які розібрано. Почніть з базового образу: оновлення FROM часто прибирає 80% знахідок за один коміт.

5. SBOM і підпис образів

SBOM (Software Bill of Materials) — перелік усіх компонентів образу. Коли з'являється нова критична вразливість, SBOM за хвилини відповідає на питання «де в нас використовується ця бібліотека?».

# Згенерувати SBOM у форматі CycloneDX
trivy image --format cyclonedx --output sbom.json ghcr.io/acme/api:1.4.2

# Підписати образ без довгоживучих ключів (keyless через OIDC у CI)
cosign sign ghcr.io/acme/api@sha256:3f1c...

# Перевірити підпис перед деплоєм
cosign verify ghcr.io/acme/api@sha256:3f1c... \
  --certificate-identity-regexp 'https://github.com/acme/api/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

У Kubernetes політики допуску (Kyverno, Sigstore policy-controller) можуть забороняти запуск непідписаних образів.

6. Обмеження під час запуску

# Kubernetes securityContext
securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
  seccompProfile:
    type: RuntimeDefault
# Те саме для docker run
docker run --read-only --tmpfs /tmp \
  --cap-drop ALL --security-opt no-new-privileges \
  --user 10001 myapp:1.4.2
Обережно

Ніколи не запускайте робочі контейнери з --privileged і не монтуйте /var/run/docker.sock у контейнер застосунку. Доступ до Docker-сокета — це фактично root на хості.

Чекліст

  • мінімальний базовий образ, оновлюється щонайменше раз на місяць;
  • USER з числовим UID, не root;
  • .dockerignore, жодних секретів у шарах;
  • сканування на CRITICAL/HIGH у CI і за розкладом;
  • SBOM і підпис для образів, що йдуть у продакшен;
  • read-only файлова система, drop ALL capabilities, без privileged.
Чому монтування /var/run/docker.sock у контейнер небезпечне?

Через сокет можна керувати Docker-демоном хоста: запустити новий контейнер з --privileged, змонтувати кореневу файлову систему хоста і отримати повний контроль над сервером. Тобто компрометація такого контейнера дорівнює root на хості.

Збережіть статтю собі

Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.

Увійти через Google Інші способи входу