Короткий ответ

Секрет в образе — это инцидент. Слой виден в 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 environmentinspect, sockротация + secrets/file
volume app_datamountне путать
gitистория gitgit filter + ротация

Возможные причины

  1. ENV/ARG пароля в Dockerfile.
  2. COPY .env / COPY id_rsa.
  3. docker build --build-arg SECRET=.
  4. Мультистейдж, но секрет в финальном stage.
  5. git clone https://user:pat@... в RUN.
  6. Отладочный echo в entrypoint, запечённый в образ.
  7. Редко: 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 и не ротировать?

Нет, если он был в образе. Образ уже скачали.