Короткий ответ
Секрет в образе — это инцидент. Слой виден в docker history, на registry.example:5000, у всех кто pull. Порядок: 1) считать секрет скомпрометированным и ротировать (БД, API, registry htpasswd); 2) убрать секрет из Dockerfile/контекста; 3) пересобрать так, чтобы старого слоя не было в новом digest; 4) выложить новый тег, пересоздать контейнеры проекта app; 5) удалить старые теги из registry и с хостов точечно. docker system prune -a не «затирает историю в git и у разработчиков». Volume app_data не содержит эту проблему, не rm его.
Симптомы и как отличить
docker history registry.example:5000/app/web:1.2.3строкиENV DB_PASSWORD=...илиARG;docker run --rm --entrypoint cat image /app/.envпечатает ключи;- секрет в CI логе
RUN echo $TOKEN; - путают с env контейнера runtime (
environment:в compose) — это тоже плохо наdocker inspect, но не слой образа; чинится без rebuild, но секрет уже видели кто имел docker.sock.
| Где секрет | Кто видит | Действие |
|---|---|---|
| слой образа | любой pull | ротация + rebuild |
| compose environment | inspect, sock | ротация + secrets/file |
| volume app_data | mount | не путать |
| git | история git | git filter + ротация |
Возможные причины
ENV/ARGпароля в Dockerfile.COPY .env/COPY id_rsa.docker build --build-arg SECRET=.- Мультистейдж, но секрет в финальном stage.
git clone https://user:pat@...в RUN.- Отладочный
echoв entrypoint, запечённый в образ. - Редко:
docker commitс env.
Диагностика
1. История и файлы
docker history --no-trunc registry.example:5000/app/web:1.2.3 | less
docker image inspect registry.example:5000/app/web:1.2.3 --format '{{json .Config.Env}}'Поиск файлов:
docker run --rm --entrypoint sh registry.example:5000/app/web:1.2.3 -c 'grep -R --binary-files=without-match -E "PASSWORD|BEGIN OPENSSH|SECRET" /app /etc 2>/dev/null | head'Не гоняйте это на проде с выводом в общий чат.
2. Контекст сборки
cd /opt/app
grep -n -E 'ENV |ARG |COPY |\.env|PASSWORD|SECRET|TOKEN' Dockerfile compose.yaml || true
cat .dockerignoreНет .dockerignore на .env — классика.
3. Registry
Считайте, что тег уже у puller-хостов. Список тегов с тем же слоем — в UI/API registry.
Решение
Сценарий A. Ротация (обязательна первой)
Смените пароль БД, ключи API, токены CI, пароль htpasswd registry, JWT signing keys — всё, что светилось. Старый секрет в слое останется, поэтому ротация не опция.
Сценарий B. Убрать из Dockerfile
Плохо:
ARG DB_PASSWORD
ENV DB_PASSWORD=$DB_PASSWORD
COPY .env /app/.envНорма: секрет не в образе. Runtime: Docker Compose secrets: (файл 600 на хосте) или внешний vault, читаемый entrypoint. BuildKit:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ciСборка:
docker build --secret id=npmrc,src=/home/admin/.npmrc -t registry.example:5000/app/web:1.2.4 .Секрет не попадает в историю слоёв.
Сценарий C. Пересборка без старого слоя
Измените Dockerfile так, чтобы слой с секретом не переиспользовался (не достаточно нового тега на том же ID).
cd /opt/app
docker compose build --no-cache web
docker compose push web
docker compose up -d web--no-cache на затронутый stage минимум. Проверка:
docker history --no-trunc registry.example:5000/app/web:1.2.4 | grep -i pass || echo 'no pass in history'Сценарий D. Снести старые теги
На registry удалите :1.2.3 по процедуре вашего registry (garbage collect отдельно). На хостах:
docker image rm registry.example:5000/app/web:1.2.3Не prune -a как замена учёта тегов. GC registry не мгновенный.
Сценарий E. Runtime env в inspect
Вынесите в секрет-файл:
secrets:
db_password:
file: /opt/app/secrets/db_password
services:
web:
secrets:
- db_passwordПрава каталога secrets 700, не в git. Ротация файла + recreate.
Слой с секретом остаётся в registry, в кэше CI, на ноутбуках, в снапшотах VM. Ротация закрывает использование секрета. Удаление тега закрывает новые pull. Это разные цели, обе нужны. Не пишите в тикет «rmi на проде = инцидент закрыт».
BuildKit --secret монтирует файл только на время RUN. Если в том же RUN вы cat секрета в лог CI — секрет снова в журнале сборки. Запретите в pipeline печать env. RUN --mount=type=secret плюс set -x в shell напечатает командную строку, не обязательно содержимое файла, но echo $(cat /run/secrets/x) напечатает. Code review Dockerfile обязателен.
Compose environment: DB_PASSWORD: ${DB_PASSWORD} попадает в inspect и в docker compose config. Это не слой образа, но любой член группы docker читает. Вынесите в secrets: или в файл, читаемый entrypoint с правами 400. После утечки через inspect — всё равно ротация: сокет уже был.
Не используйте docker save «бэкап образа» на USB без учёта, что внутри слои с секретом. Шифруйте архив или не сохраняйте старые теги.
Как проверить, что проблема устранена
docker history --no-trunc registry.example:5000/app/web:1.2.4
docker image inspect registry.example:5000/app/web:1.2.4 --format '{{json .Config.Env}}'
docker compose -f /opt/app/compose.yaml exec web sh -c 'test ! -f /app/.env && echo no_dotenv'Нет паролей в history/Env образа. Приложение работает с новым секретом. Старый тег недоступен docker pull (404/не найден).
Если не помогло
- Старый слой в другом теге
:latest— переберите теги. - Секрет в git — ротация + чистка истории git, это уже VCS.
- BuildKit cache на builder-хосте хранит секрет —
docker builder pruneна сборочном узле после фикса, не на проде без нужды. - Кто-то снова
COPY .envв ветке feature.
Профилактика
- CI grep Dockerfile на
ENV *PASS*,COPY .env. .dockerignore:.env,*.pem,id_rsa.- Только BuildKit secrets / runtime secrets.
- Scan образов (Trivy и аналоги) на private keys.
- Hardening запрещает inspect-доступ всем подряд (сокет).
FAQ
Достаточно ли docker rmi старого образа на одном хосте?
Нет. Registry и ноутбуки разработчиков. Ротация закрывает использование секрета.
ARG без ENV безопаснее?
ARG тоже в history того слоя. Не для паролей.
Многостадийная сборка спасает?
Только если секрет не копируется в финальный stage и не засвечен в history финальных слоёв. RUN с секретом в builder stage всё ещё в history builder-образа, если вы его пушите.
prune -a удалит секрет из registry?
Нет.
Можно ли chown секрета в volume и не ротировать?
Нет, если он был в образе. Образ уже скачали.