Модуль 08 — Реестры и безопасность

Куда класть образы, как не выложить вместе с ними пароли и как узнать, что в них уязвимо

Прогресс курса Модуль 8 из 9

Что вы освоите в этом модуле

01

Зачем реестр и какие бывают

Образ, собранный на ноутбуке, нужно доставить на серверы. Реестр (registry) — сервер, который хранит образы и отдаёт их по имени и тегу. Сборка в конвейере CI кладёт образ в реестр, серверы забирают его оттуда: исходный код и инструменты сборки на боевой сервер не попадают.

РеестрАдрес в имени образаОсобенности
Docker Hubdocker.io/… (можно опускать)Официальные образы; ограничения доступа из России (модуль 01)
Yandex Container Registrycr.yandex/<id реестра>/…Российское облако, оплата в рублях, доступ по ключам сервисных аккаунтов
Реестр GitLabregistry.<сервер gitlab>/…Встроен в GitLab, в том числе в установленный у себя; удобен с GitLab CI
GitHub Container Registryghcr.io/…Рядом с кодом на GitHub
Harbor, Nexus, Docker Registryсвой доменСтавятся у себя; Harbor умеет роли, сканирование и зеркалирование

Для компаний, работающих с персональными данными и требованиями к их размещению в России, реестр в российском облаке или свой реестр часто не выбор, а условие.

02

Свой реестр за минуту

Официальный образ registry — реестр, который можно поднять где угодно. Чтобы отправить образ в реестр, имя образа должно начинаться с адреса реестра — добавим такой тег и выполним push:

$ docker run -d --name registry -p 127.0.0.1:5000:5000 -v registry-data:/var/lib/registry registry:3
726f43e8076e3be887f1555ab612123203a3def16ac5f6b4cabe912784cf3721
$ docker tag notes:2.0.0 localhost:5000/notes:2.0.0
$ docker push localhost:5000/notes:2.0.0 | grep -v -E "Preparing|Waiting"
The push refers to repository [localhost:5000/notes]
be64570a34e4: Pushed
2.0.0: digest: sha256:08a3b608be9a475edc358e3a8db5e76cd2fafab5da0086d266305c2bc5d407d9 size: 527

Отправляются слои; слой, который уже есть в реестре, повторно не отправляется — второй образ на той же базе python:3.13-slim загрузится за секунды. В конце — дайджест: под этим адресом образ будет неизменным. У реестра простой HTTP API:

$ curl -s http://localhost:5000/v2/_catalog
{"repositories":["notes"]}
$ curl -s http://localhost:5000/v2/notes/tags/list
{"name":"notes","tags":["2.0.0"]}

Удалим образ локально и заберём из реестра — так сервер получает новую версию:

$ docker image rm localhost:5000/notes:2.0.0
Untagged: localhost:5000/notes:2.0.0
Untagged: localhost:5000/notes@sha256:08a3b608be9a475edc358e3a8db5e76cd2fafab5da0086d266305c2bc5d407d9
$ docker pull localhost:5000/notes:2.0.0
2.0.0: Pulling from notes
Digest: sha256:08a3b608be9a475edc358e3a8db5e76cd2fafab5da0086d266305c2bc5d407d9
Status: Downloaded newer image for localhost:5000/notes:2.0.0
localhost:5000/notes:2.0.0
$ docker image inspect -f "{{json .RepoDigests}}" localhost:5000/notes:2.0.0
["localhost:5000/notes@sha256:08a3b608be9a475edc358e3a8db5e76cd2fafab5da0086d266305c2bc5d407d9"]
$ docker rm -f registry
registry
Docker разговаривает без TLS только с реестрами на localhost. Для реестра на другом сервере нужен HTTPS с настоящим сертификатом — обычно за обратным прокси nginx. Параметр insecure-registries в daemon.json отключает проверку, и тогда образ можно подменить по дороге — в эксплуатации так не делают.
03

Вход в реестр и где лежит пароль

Реестр без пароля разрешает всем и читать, и писать. Простейшая защита — файл htpasswd с хешами паролей; сделаем его утилитой из образа Apache и запустим реестр с ним. Теперь без входа образ не скачать; docker login — вход:

$ mkdir -p auth; docker run --rm --entrypoint htpasswd httpd:2.4-alpine -Bbn student Учеба-2026 > auth/htpasswd; cut -c1-30 auth/htpasswd
student:$2y$05$EDksM9Qm9fIR9On

$ docker run -d --name registry -p 127.0.0.1:5000:5000 -v registry-data:/var/lib/registry -v "$PWD/auth":/auth:ro -e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_REALM=nevabit -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry:3
035cfeb1fcee454a152c2624b9b5ff367607b302fcef17ad2a1cd860f6847b83
$ docker pull localhost:5000/notes:2.0.0
Error response from daemon: Head "http://localhost:5000/v2/notes/manifests/2.0.0": no basic auth credentials
$ echo "Учеба-2026" | docker login localhost:5000 -u student --password-stdin
WARNING! Your password will be stored unencrypted in /tmp/dkpage.gOdJgs/ex/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
$ cat .docker/config.json
{
	"auths": {
		"localhost:5000": {
			"auth": "c3R1ZGVudDrQo9GH0LXQsdCwLTIwMjY="
		}
	}
}$ grep -o "\"auth\": \"[^\"]*\"" .docker/config.json | cut -d\" -f4 | base64 -d; echo
student:Учеба-2026
$ docker logout localhost:5000
Removing login credentials for localhost:5000
$ docker rm -f registry
registry

Docker сам предупреждает: пароль сохранён незашифрованным. Поле auth — это логин:пароль в Base64, который раскодирует кто угодно. Файл ~/.docker/config.json — частая добыча: его забывают в резервных копиях, домашних каталогах общих серверов, в образах (COPY . . из домашнего каталога).

04

Секреты в образе

Частая ошибка — передать при сборке токен (для закрытого репозитория пакетов, для скачивания модели) и считать, что его «потом удалили». Образ для опыта: токен приходит через ARG и через файл, файл удаляется в той же инструкции:

FROM alpine:3.22
ARG API_TOKEN
COPY token.txt /tmp/token.txt
RUN . /tmp/token.txt && echo "скачиваю с токеном" && rm /tmp/token.txt
$ docker build -q --build-arg API_TOKEN=secret-arg-42 -f Dockerfile.leak -t leak . >/dev/null
$ docker run --rm leak ls /tmp
$ docker history --no-trunc --format "{{.CreatedBy}}" leak | grep -m1 API_TOKEN
RUN |1 API_TOKEN=secret-arg-42 /bin/sh -c . /tmp/token.txt && echo "скачиваю с токеном" && rm /tmp/token.txt # buildkit
$ mkdir img; docker save leak | tar -x -C img; for l in img/blobs/sha256/*; do tar -tf $l 2>/dev/null | grep -qx tmp/token.txt && tar -xOf $l tmp/token.txt; done
TOKEN=demo-1234567890

В запущенном контейнере файла нет — ls /tmp пуст. Но:

Всё, что когда-либо попало в любой слой, есть у каждого, кто может скачать образ. Это касается и ENV, и файлов .env, скопированных через COPY . ..

Правильный способ — секрет сборки BuildKit. Файл подключается только на время одной инструкции RUN в /run/secrets/ и не попадает ни в слой, ни в историю:

FROM alpine:3.22
RUN --mount=type=secret,id=token \
    . /run/secrets/token && echo "скачиваю с токеном, длина ${#TOKEN}"
$ docker build --no-cache --progress=plain --secret id=token,src=token.txt -f Dockerfile.secret -t secret . 2>&1 | grep "длина"
#5 [stage-0 2/2] RUN --mount=type=secret,id=token     . /run/secrets/token && echo "скачиваю с токеном, длина ${#TOKEN}"
#5 0.188 скачиваю с токеном, длина 15
$ docker history --no-trunc --format "{{.CreatedBy}}" secret | head -1
RUN /bin/sh -c . /run/secrets/token && echo "скачиваю с токеном, длина ${#TOKEN}" # buildkit

Токен прочитан (длина 15 символов), а в истории — только команда. Секреты для работы приложения (пароль базы, ключи API) в образ не попадают вовсе: они передаются при запуске — переменными из .env на сервере или, надёжнее, через secrets: в Compose (модуль 06).

05

Сканирование уязвимостей

В базовом образе — десятки и сотни пакетов, и в них регулярно находят уязвимости. Сканер сверяет версии пакетов образа с базами известных уязвимостей (CVE). Trivy — открытый сканер, удобно запускается сам как контейнер; ему нужен доступ к Docker, чтобы прочитать образ, и кэш для базы уязвимостей:

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$HOME/.cache/trivy":/root/.cache aquasec/trivy:0.67.2 image -q --scanners vuln --format table python:3.13-slim 2>&1 | sed -n "/Report Summary/,/Legend/p"
Report Summary

┌──────────────────────────────────────────────────────────────────────┬────────────┬─────────────────┐
│                                Target                                │    Type    │ Vulnerabilities │
├──────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ python:3.13-slim (debian 13.7)                                       │   debian   │       164       │
├──────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ Python                                                               │ python-pkg │        6        │
├──────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ usr/local/lib/python3.13/site-packages/pip-26.2.2.dist-info/METADATA │ python-pkg │        0        │
└──────────────────────────────────────────────────────────────────────┴────────────┴─────────────────┘
Legend:
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$HOME/.cache/trivy":/root/.cache aquasec/trivy:0.67.2 image -q --scanners vuln --format table python:3.13 2>&1 | sed -n "/Report Summary/,/Legend/p" | head -8
Report Summary

┌──────────────────────────────────────────────────────────────────────┬────────────┬─────────────────┐
│                                Target                                │    Type    │ Vulnerabilities │
├──────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ python:3.13 (debian 13.7)                                            │   debian   │      3492       │
├──────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ Python                                                               │ python-pkg │        6        │

Слим-образ — 170 записей, полный — больше 3 400: каждый лишний пакет добавляет уязвимостей, хотя приложение им не пользуется. Это ещё один довод за -slim и многоэтапную сборку из модуля 07. Числа — на 23.10.2026; сканируйте сами — завтра они будут другими.

Как с этим работать:

Базу уязвимостей Trivy скачивает с ghcr.io и mirror.gcr.io; 23.10.2026 загрузка с учебного сервера работала. Для закрытых контуров Trivy умеет работать с заранее скачанной базой.

06

Сокет Docker в контейнере — ключи от сервера

Некоторым программам нужен доступ к Docker: сканеру, панели управления, агенту CI, обратному прокси Traefik. Их запускают, подключив в контейнер сокет хоста по тому же пути — /var/run/docker.sock. Но сокет — это полный доступ к службе Docker (модуль 01), а значит к хосту. Изнутри такого контейнера видно и можно сделать всё:

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:26-cli docker ps --format "{{.Names}}"
brave_dhawan
db-prod
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:26-cli docker run --rm -v /:/host alpine:3.22 cat /host/etc/hostname
mypc

Контейнер с сокетом увидел соседей, в том числе db-prod, и запустил новый контейнер с корнем хоста — дальше можно читать базы, ключи, менять системные файлы. Проброс :ro не помогает: «только чтение» относится к файлу сокета, а не к командам API.

07

Без root: rootless Docker и Podman

Корень многих проблем — служба dockerd работает от root. Есть два способа обойтись без этого:

Даже с обычным Docker часть защиты включается одной строкой в daemon.json: "userns-remap": "default" — root контейнеров отображается на непривилегированный диапазон UID хоста. После включения Docker хранит данные в отдельном подкаталоге /var/lib/docker, и прежние образы, контейнеры и тома перестают быть видны, — поэтому его включают на новом сервере, а не на работающем.

08

Проверочный список

ГдеЧто проверить
DockerfileБазовый образ -slim или меньше, с конкретным тегом; многоэтапная сборка; USER не root; .dockerignore с .env и ключами; секреты только через --secret
ОбразСканирование без CRITICAL; docker history без паролей; регулярная пересборка с --pull
Запускcap_drop: [ALL], no-new-privileges, read_only, лимиты памяти и процессора, без --privileged и без сокета Docker
СетьПубликуется только прокси; остальное — 127.0.0.1 или без портов; база в отдельной сети
СерверГруппа docker — только администраторы; API Docker не открыт в сеть; ротация журналов; токены реестра только на чтение

Ключевые выводы модуля

Запомните главное

  • Имя образа для реестра начинается с адреса реестра; push отправляет только недостающие слои
  • Чужой реестр — только по HTTPS; config.json хранит пароль в Base64 — помощники хранения и токены только на чтение
  • Всё, что попало в слой, ARG или ENV, утекает вместе с образом; секреты сборки — RUN --mount=type=secret
  • Сканировать образы (Trivy) и регулярно пересобирать с --pull
  • docker.sock в контейнере = root на хосте
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 07: Образы для эксплуатации Все модули Итоговый проект