Статья Сбор данных из Docker-образов: практика и анализ

xzotique

Grey Team
24.03.2020
127
463
1770067987699.webp


Ты, вероятно, как и я, устал от нарратива. От этой всеобщей эйфории, где каждый второй мануал начинается с docker run hello-world, а каждый первый архитектор рисует в креативе бесконечные облака с коробочками, соединенными стрелочками. Мир, где сложность спрятали за абстракциями, а безопасность - за галочками в Saas-панельке какого-нибудь сканера.

Слышишь этот гул? Это гул тысяч docker pull, бездумно исполняемых в CI/CD пайплайнах. Это шепот FROM node:latest в начале бесчисленных Dockerfile. Это ритуальное заклинание современной разработки: «Оно работает в контейнере, значит, оно изолировано, значит, оно безопасно». И все дружно кивают, не вдаваясь в детали. Потому что детали - это скучно. Потому что детали - это не про масштабирование и time-to-market. А мы с тобой — про детали. Нас не устраивает магия, пока мы не разберем фокус до винтика.

Давай посмотрим правде в глаза: Docker (и вся контейнерная экосистема) - это один из величайших парадоксов нашей индустрии. Инструмент, рожденный из философии UNIX и идеи легковесной изоляции, превратился в фабрику по производству церемониальных зависимостей и, что гораздо хуже, в кладезь критических секретов. Публичный реестр образов - это не аккуратная библиотека. Это гибрид помойки и музея. На одной полке лежит nginx в стоковой сборке, на другой - legacy-backend-service какого-нибудь умершего fintech-стартапа, внутри которого, как в капсуле времени, застыли ключи от его банковских API. Рядом - ci-runner с захардкоженным токеном GitLab, test-db с дампом реальных (анонимизированных? хах, хорошая шутка) пользовательских данных, и сотни my-app от инженеров, которые просто хотели попробовать эту штуку с Docker и забыли про выложенный на Hub образ.

Почему это происходит? Потому что общество продаёт простоту. «Собери раз, запускай где угодно». Но оно не продает понимание цены этой простоты. Слой за слоем, команда за командой, мы упаковываем в образ не только наше приложение, но и весь контекст его создания: временные файлы, кэши, ключи для доступа к зависимостям, конфиги с локальной машины. Мы делаем это на автомате, потому что COPY . . - это просто. А потом, когда все работает, мы делаем docker push - и наш цифровой отпечаток, со всеми его родинками и шрамами, улетает в публичное пространство. Навсегда. Или до тех пор, пока кто-то вроде нас не решит покопаться.

Кто этот «кто-то вроде нас»? Это не обязательно злобный хакер в капюшоне. Это может быть:
  • Аудитор безопасности, проверяющий поверхность атаки компании.
  • Ответственный инженер, оценивающий риски от использования сторонних образов.
  • Любопытный исследователь, изучающий реальные практики разработки.
  • Скрипт-кидди, запустивший сканер с GitHub и радостно наблюдающий, как тот выплевывает найденные AWS-ключи.
Это методичное, почти скучное погружение в недра формата, который все используют, но мало кто действительно понимает. Мы не будем полагаться на волшебную кнопку «Scan». Мы возьмем образ, как архив, и разберем его по слоям, по байтам, по метаданным. Мы напишем свои инструменты не потому, что нет готовых, а потому что только так можно понять процесс от и до. Мы будем иронизировать над небрежностью, но не будем циничными - потому что каждый найденный нами артефакт это не повод для злорадства, а повод для внутренней проверки: «А у меня так?».

Готовься к долгой дороге. Мы поговорим о том:
  • Как устроен образ на физическом уровне (это не просто «виртуальный диск», чувак).
  • Почему docker history - это лишь приветственная открытка, а реальная история написана в самих слоях.
  • Как метаданные и временные метки могут рассказать больше, чем содержимое файлов.
  • Какие инструменты использовать не потому, что они модные, а потому что они решают конкретную задачу: jq, tar, binwalk, dive, skopeo, и конечно, наш верный Bash и Python.
  • Как написать свой анализатор, который будет искать не только по шаблонам, но и понимать контекст.
  • Что реально находят в этих образах (и ты удивишься банальности и масштабу этих находок).
  • Как строить свою защиту не на страхе, а на архитектуре: multi-stage сборки, BuildKit secrets и менталитет «нулевого доверия к образу».
Мы спускаемся в кроличью нору, и на другом конце нас ждет не страна чудес, а сырые, необработанные данные.
И понимание. Именно то, что отличает ремесленника от пользователя. Погнали.

Часть 1: Философия «слоеного пирога» и почему docker images врет

Контейнер - это не виртуальная машина. Забудь. Это изоляция на уровне процессов, построенная на cgroups и namespaces. Образ - это набор read-only слоев, накрытых верхним слоем. Каждая команда в Dockerfile (RUN, COPY, ADD) создает новый слой. И вот здесь начинается магия и одновременно - трагедия.

Когда ты делаешь docker pull nginx:latest, ты качаешь не монолитную картинку. Ты качаешь связку диффов. И многие из этих диффов содержат файлы, которые в последующих слоях были удалены. Да, ты правильно прочитал. Удаление файла в Docker - это не физическое удаление из нижележащего слоя. Это создание нового слоя, в котором этого файла «как бы нет». Но старый слой с ним остается. Он все еще там, в образе. Он все еще скачивается при pull. И он все еще доступен для того, кто знает, как его достать.

Распространённые инструменты сканирования (docker scan, Trivy, Clair) часто смотрят только на финальный слой или на установленные пакеты. Они ищут уязвимости в libc. Но они пропускают файл .env, скопированный на третьем слое и удаленный на пятом. Они пропускают SSH-ключ в /root/.ssh/id_rsa, добавленный для клонирования приватного репозитория и «удаленный» потом. Они пропускают историю bash в /root/.bash_history, которая не затерлась.

Наша первая и главная заповедь: Контейнерный образ - это не текущее состояние файловой системы. Это хронологическая летопись всех операций, которые совершал над ним создатель. И эту летопись можно прочитать.


Часть 2: Инструментарий археолога. Не Skynet, а Bash и Python.

Забудь про дорогие Enterprise-решения на первые 15 минут анализа. Наш базовый набор - это то, что есть везде, или ставится в два клика.

1. docker и docker save - наш швейцарский нож.
Первое, что нужно сделать - вытащить образ из облака к себе локально, в сыром виде.

Bash:
docker pull интересный/образ:тег
docker save интересный/образ:тег -o образ.tar

Теперь у тебя есть .tar архив. Распакуй его:

Bash:
mkdir image_extract && cd image_extract
tar -xf ../образ.tar

Внутри ты увидишь кучу файлов: manifest.json, repositories, и папки со странными хеш-именами - это и есть слои. Каждая такая папка - это tar архив слоя. manifest.json - это инструкция по сборке конечной файловой системы: в каком порядке накладывать слои.

2. jq - твой лучший друг для разбора JSON.
Без него никуда. Установи (apt install jq, brew install jq). С его помощью мы будем парсить manifest.json и другие мета-файлы.

3. Простой Python/Bash для автоматизации.
Мы не ищем сложные фреймворки. Мы напишем несколько скриптов, которые делают ровно то, что нам нужно. Я приведу куски кода, ты их соберешь воедино под свои нужды.

4. binwalk, strings, grep, find - классика, которая никогда не стареет.
Именно они будут искать в бинарных слоях текстовые строки, паттерны, знакомые форматы.

5. Специализированные утилиты: dive и skopeo.

Часть 3: Практика. От слоя к слою, от артефакта к артефакту.

Давай перейдем от теории к практике. Представим, что мы нашли подозрительный/интересный публичный образ somecompany/internal-tool:latest. Наша цель - исследовать его вдоль и поперек.

Шаг 0: Предварительный осмотр с docker history и docker inspect.
Быстро и грязно:

Bash:
docker history --no-trunc somecompany/internal-tool:latest
Флаг --no-trunc критически важен! Без него длинные команды RUN будут обрезаны, и ты пропустишь самое интересное (например, curl с токеном). В выводе смотри на команды RUN, COPY, ADD. Ищи что-то вроде:

Bash:
RUN apt-get update && apt-get install -y curl && curl -H "Authorization: Bearer ghp_xxxxxxxxxx" https://api.github.com/repos/... && rm -rf /var/lib/apt/lists/*
Увидел токен? Уже победа. Но это только видимая часть истории. docker history показывает команды, но не показывает содержимое копируемых файлов.

docker inspect даст нам мета-информацию: переменные окружения (Env), точку входа (Cmd, Entrypoint), рабочий каталог (WorkingDir). Иногда в Env уже могут быть жестко зашитые пароли или URLs с креденшелами.

Шаг 1: Выгрузка и разбор образа.
Делаем, как описано выше:

Bash:
docker pull somecompany/internal-tool:latest
docker save somecompany/internal-tool:latest -o internal-tool.tar
mkdir -p analysis && cd analysis
tar -xvf ../internal-tool.tar

Теперь смотрим в manifest.json:

Bash:
cat manifest.json | jq .
На выходе получим что-то вроде:

JSON:
[{
  "Config": "a123...bcd.json",
  "RepoTags": ["somecompany/internal-tool:latest"],
  "Layers": ["layer1.tar/sha256:...", "layer2.tar/sha256:...", ...]
}]

Запоминаем имя конфига (a123...bcd.json). Он содержит историю команд и параметры сборки, аналогичные docker inspect. Слои перечислены в порядке наложения: первый в списке - самый нижний, последний - верхний.

Шаг 2: Распаковка и анализ каждого слоя.
Пишем небольшой bash-скрипт unpack_layers.sh:

Bash:
#!/bin/bash
MANIFEST="manifest.json"
CONFIG=$(jq -r '.[0].Config' $MANIFEST)
echo "Конфиг: $CONFIG"
jq . $CONFIG # Смотрим конфиг

LAYERS=$(jq -r '.[0].Layers[]' $MANIFEST)
echo "Слои:"
echo "$LAYERS"

# Создаем директорию для каждого слоя и распаковываем его
for LAYER in $LAYERS; do
    LAYER_NAME=$(basename $LAYER .tar)
    echo "Работаем со слоем: $LAYER_NAME"
    mkdir -p "layers/$LAYER_NAME"
    tar -xf "$LAYER" -C "layers/$LAYER_NAME/"
done

Запускаем. Теперь у нас есть папка layers/, в которой подпапки с именами слоев, и внутри - актуальная файловая система на момент этого слоя.

Шаг 3: Поисковая археология.
Вот где начинается самое интересное. Мы должны искать не только в последнем слое, а во всех. Напишем search_artifacts.sh:

Bash:
#!/bin/bash
SEARCH_DIR="layers"
OUTPUT_FILE="findings.txt"

echo "# Отчет по артефактам в образах" > $OUTPUT_FILE

# 1. Ищем файлы с определенными именами (классика)
echo "### Файлы с подозрительными именами ###" >> $OUTPUT_FILE
find $SEARCH_DIR -type f \( -iname "*.pem" -o -iname "*.key" -o -iname "id_rsa" -o -iname "id_dsa" -o -iname "*.ppk" -o -iname "*.env" -o -iname ".git-credentials" -o -iname "*.kdbx" -o -iname "config" -path "*ssh*" \) 2>/dev/null | sort >> $OUTPUT_FILE

# 2. Ищем файлы с определенным содержимым (паттерны)
echo -e "\n### Файлы, содержащие паттерны (пароли, токены, ключи) ###" >> $OUTPUT_FILE
# Проходим по всем распакованным файлам
for LAYER in $SEARCH_DIR/*; do
    echo "Сканируем слой: $(basename $LAYER)" >> $OUTPUT_FILE
    # Ищем строки, похожие на API keys, JWTs, пароли
    # Важно: grep может долго работать на бинарниках, но оно того стоит
    grep -r -E -i "(password|passwd|pwd|secret|key|token|auth|bearer|api[_-]?key|access[_-]?token|gh[pus]_|aws[_-]|AKIA|secret_?key|private[_-]?key|ssh-rsa|BEGIN RSA|BEGIN OPENSSG|BEGIN PRIVATE)" "$LAYER" 2>/dev/null | grep -v "Binary file" | head -20 >> $OUTPUT_FILE
done

# 3. Анализ истории bash (если есть)
echo -e "\n### Содержимое файлов истории оболочки ###" >> $OUTPUT_FILE
find $SEARCH_DIR -type f -name ".bash_history" -o -name ".zsh_history" 2>/dev/null | while read hist; do
    echo "--- $hist ---" >> $OUTPUT_FILE
    tail -50 "$hist" >> $OUTPUT_FILE 2>/dev/null
done

# 4. Поиск дампов баз данных, sql файлов
echo -e "\n### Файлы баз данных и SQL дампы ###" >> $OUTPUT_FILE
find $SEARCH_DIR -type f \( -iname "*.db" -o -iname "*.sqlite" -o -iname "*.sql" -o -iname "*.dump" -o -iname "*.bak" \) 2>/dev/null >> $OUTPUT_FILE

# 5. Поиск конфигов облачных провайдеров
echo -e "\n### Конфигурации облаков и инструментов ###" >> $OUTPUT_FILE
find $SEARCH_DIR -type f \( -path "*/.aws/credentials" -o -path "*/.azure/*" -o -path "*/.kube/config" -o -path "*/.docker/config.json" -o -name "credentials.xml" -o -name "settings.xml" \) 2>/dev/null >> $OUTPUT_FILE

echo "Поиск завершен. Результаты в $OUTPUT_FILE"

Этот скрипт - наше основное орудие. Он грубый, но эффективный. Он найдет много ложных срабатываний (например, слово «private» в документации), но лучше ложное срабатывание, чем пропущенный ключ.

Шаг 4: Глубокий анализ бинарных слоев.
Некоторые артефакты могут быть зашиты в бинарники или в архив внутри слоя. Тут нам помогут binwalk и strings.

Bash:
# Для каждого слоевого tar-архива (до распаковки) можно проверить на вложенность
for LAYER_TAR in *.tar; do
    echo "Анализ архива слоя: $LAYER_TAR"
    binwalk "$LAYER_TAR" | head -30
    # Или ищем строки прямо в tar
    strings "$LAYER_TAR" | grep -E "(passw|token|key|secret)" | head -10
done

Шаг 5: Воссоздание «удаленных» файлов.
Помнишь про удаление? Чтобы увидеть файл, который был добавлен в слое N, а удален в слое M>N, нам нужно посмотреть на файловую систему слоя N. Наш скрипт find уже делает это, потому что обходит все распакованные слои. Но давай визуализируем:
Допустим, в layers/layer3/ мы видим файл /app/config.json. В layers/layer5/ этого файла уже нет. Значит, он был удален командой RUN rm /app/config.json в слое 5. Но в слое 3 он лежит целиком и полностью. Мы его просто копируем оттуда.

Шаг 6: Анализ метаданных и временных меток.
Иногда важно не только «что», но и «когда». Временные метки файлов в слоях могут подсказать, в каком порядке велась разработка, были ли в образ включены файлы, созданные вне процесса сборки (например, скопированные с локальной машины). Для этого можно использовать stat внутри каждого слоя.


Часть 4: Живые примеры - что находили в реальном мире (обезличенно).

Ты не поверишь, что люди заливают. Я сам иногда не верю. Вот лишь несколько категорий находок из публичных образов на Docker Hub:
  1. Ключи доступа к облаку: Файлы ~/.aws/credentials с активными AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY. Образы для деплоя в AWS, собранные на CI-сервере, где эти переменные были в окружении и «случайно» просочились в слой. Или gcloud конфиги.
  2. Токены GitHub/GitLab: RUN git clone https://oauth2:ghp_xxxxx@github.com/company/private-repo. Токен в команде RUN виден в docker history, но его же могут закопировать из локального ~/.git-credentials.
  3. SSH ключи: Полные пары id_rsa и id_rsa.pub в /root/.ssh/. Часто для доступа к приватным репозиториям или хостам при сборке.
  4. Переменные окружения в plain text: Файлы .env, application.properties, config.yaml, лежащие прямо в образе со всеми паролями к базам данных, API-ключами внешних сервисов.
  5. Дампы и бэкапы: Полноценные SQL-дампы производственных баз данных в /tmp или /docker-entrypoint-initdb.d/, оставленные для инициализации тестовых сред, но забытые там.
  6. Исходный код внутренних библиотек: В образах для сборки часто копируется весь контекст, включая vendor/, node_modules/, и иногда - субмодули или приватные пакеты, которые не должны были уйти за пределы компании.
  7. Конфиги оркестраторов: Файлы kubeconfig для доступа к кластерам Kubernetes. Одна находка - и ты внутри продакшн-сети.
  8. Креденшелы к внутренним репозиториям артефактов: settings.xml для Maven или .npmrc с токенами к приватным npm-реестрам.
Это не теории заговора. Это ежедневная реальность. Поищи на GitHub скрипты для «Docker Hub scanning» - существуют целые проекты, которые автоматически качают и сканируют тысячи образов, находя такие утечки.


Часть 5: Защита. Как не стать донором артефактов.

Если после прочтения у тебя зашевелились волосы на затылке, потому что ты вспомнил про свои образы - отлично. Значит, сознание прочищается. Давай теперь о защите. Это не просто «используй .dockerignore» (хотя это обязательно). Это комплексный подход.

1. Многостадийная сборка (Multi-stage builds) - твой священный грааль.
Это главное оружие против утечек. Смысл в том, что у тебя несколько FROM в одном Dockerfile. Ты компилируешь/собираешь в одном образе (с полным SDK, ключами, исходниками), а потом копирушь только готовые артефакты (бинарники, jar-файлы, статику) в чистый, финальный образ (часто на alpine или scratch).

Код:
// dockerfile
# Стадия сборки
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
# Здесь могут использоваться приватные ключи для go modules
RUN go mod download
RUN go build -o myapp .
# Здесь лежит бинарник и всё, что было в исходниках и кэше

# Финальная стадия
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
# Копируем ТОЛЬКО бинарник. Ни исходников, ни ключей, ни кэша модулей.
CMD ["./myapp"]

Ключи и исходники остаются в промежуточном образе builder, который не попадает в финальный образ и не пушится в реестр (если не использовать флаг --target при пуше). Это кардинально решает 90% проблем.

2. Никаких секретов в Dockerfile. Никогда.
  • Не используй ARG для паролей, которые потом становятся ENV. ARG остаются в истории слоев.
  • Не делай RUN с curl/wget, передавая ключи в URL или заголовках. Даже если потом удалишь curl, команда будет видна в истории.
  • Не копируй файлы с секретами (*.key, .env.prod) в образ, даже чтобы потом удалить.
3. Используй Docker Secrets / BuildKit секреты.
Для передачи чувствительных данных при сборке (например, для установки приватных пакетов):

Код:
// dockerfile
# syntax=docker/dockerfile:1
FROM alpine:latest
RUN --mount=type=secret,id=npm_token,target=/root/.npmrc \
    npm install

Секрет монтируется временно только на время выполнения этой команды RUN и не попадает ни в один слой. Вызывается так:

Bash:
DOCKER_BUILDKIT=1 docker build --secret id=npm_token,src=~/.npmrc -t myimage .
4. Тщательный .dockerignore.
Этот файл должен быть таким же важным, как .gitignore. Игнорируй все, что не нужно для сборки: .git, .md, *.log, env/, secrets/, *.pem, *.key, docker-compose.yml, .idea, .vscode. Чем меньше мусора скопировано в контекст, тем меньше шансов, что что-то просочится.

5. Чистка истории и слоев.
Объединяй команды в одну RUN, чтобы минимизировать слои и точки сохранения состояния:

Код:
// dockerfile
RUN apt-get update && \
    apt-get install -y some-package && \
    rm -rf /var/lib/apt/lists/* && \
    # Делай всё, что нужно
    apt-get clean

Используй docker image prune для удаления промежуточных образов после multi-stage сборки.

6. Сканируй свои же образы перед пушем.
Используй те же инструменты, что и мы, но против себя:

Bash:
# Пример с dive для просмотра изменений в слоях
dive myimage:latest
# Сканирование на уязвимости и секреты (есть инструменты)
trivy image --security-checks secret myimage:latest
# или
grype myimage:latest

Существуют инструменты вроде git-secrets, которые можно интегрировать в CI/CD, чтобы предотвратить коммит ключей в репозиторий, а значит, и их копирование в образ.

7. Используй приватные реестры с контролем доступа.
Docker Hub - публичная площадка. Для всего внутреннего используй приватные реестры (GitLab Container Registry, Harbor, AWS ECR, GCR). Это не панацея от утечек внутри образов, но сильно сужает круг лиц, которые могут их скачать.

8. Менталитет «нулевого доверия» к образу.
Воспринимай любой образ (даже свой вчерашний) как потенциально опасный. Перед деплоем в прод проверяй его происхождение, целостность (digest), сканируй. В Kubernetes используй Admission Controllers (например, OPA Gatekeeper, Kyverno) для запрета запуска образов без определенных подписей (Cosign) или из непроверенных реестров.

1770068031644.webp


Часть 6: Автоматизация для пентеста/аудита. Собираем свой сканер.

Если ты проводишь аудит безопасности компании или хочешь мониторить утечки в образах своих зависимостей, ручной процесс не подойдет. Давай набросаем каркас простого, но мощного сканера на Python.

Мы будем использовать:
  • docker-py для низкоуровневого взаимодействия с Docker (но можно и через subprocess).
  • requests для прямого доступа к Docker Registry API (если не хочешь тянуть демон Docker).
  • Все тот же jq и tarfile из стандартной библиотеки.
План скрипта:
  1. Выбор цели: Список образов из файла или поиск через Docker Hub API (осторожно с лимитами).
  2. Скачивание: Либо через docker pull, либо напрямую качать манифест и слои по HTTP (это быстрее и не требует демона).
  3. Разбор: Распаковка слоев в временную директорию.
  4. Анализ: Запуск нашего набора эвристик (поиск файлов, grep по содержимому, анализ истории).
  5. Отчет: Вывод в структурированном виде (JSON, HTML) с указанием: образ, слой, путь, тип артефакта, предварительная оценка критичности.
Пример на Python (упрощенный каркас):

Python:
import tarfile
import json
import os
import subprocess
import tempfile
import re
from pathlib import Path

class DockerImageArchaeologist:
    def __init__(self, image_name):
        self.image_name = image_name
        self.temp_dir = tempfile.mkdtemp(prefix="docker_scan_")
        self.findings = []

    def pull_and_save(self):
        """Качает образ и сохраняет его в tar."""
        # Внимание: требует установленного docker и прав
        tar_path = os.path.join(self.temp_dir, "image.tar")
        subprocess.run(["docker", "pull", self.image_name], check=True)
        subprocess.run(["docker", "save", self.image_name, "-o", tar_path], check=True)
        return tar_path

    def extract_and_analyze(self, tar_path):
        """Распаковывает tar и анализирует слои."""
        with tarfile.open(tar_path, 'r') as tar:
            tar.extractall(path=self.temp_dir)

        manifest_path = os.path.join(self.temp_dir, "manifest.json")
        with open(manifest_path, 'r') as f:
            manifest = json.load(f)[0]  # берем первый элемент

        config_file = manifest['Config']
        layers = manifest['Layers']

        # Анализ конфига
        self._analyze_config(os.path.join(self.temp_dir, config_file))

        # Анализ каждого слоя
        for layer_tar in layers:
            layer_path = os.path.join(self.temp_dir, layer_tar)
            layer_name = os.path.basename(layer_tar).replace('.tar', '')
            self._analyze_layer(layer_path, layer_name)

    def _analyze_config(self, config_path):
        with open(config_path, 'r') as f:
            config = json.load(f)
        # Проверяем переменные окружения
        for env in config.get('config', {}).get('Env', []):
            if self._is_sensitive(env):
                self.findings.append({
                    'type': 'Sensitive Env in Config',
                    'layer': 'Config',
                    'data': env[:50] + '...'  # обрезаем для вывода
                })
        # Можно разбирать историю команд (config['history'])

    def _analyze_layer(self, layer_tar_path, layer_name):
        """Распаковывает слой и ищет артефакты."""
        layer_extract_path = os.path.join(self.temp_dir, layer_name)
        os.makedirs(layer_extract_path, exist_ok=True)

        with tarfile.open(layer_tar_path, 'r') as tar:
            tar.extractall(path=layer_extract_path)

        # 1. Поиск файлов по именам
        sensitive_patterns = ['**/.ssh/id_rsa', '**/.aws/credentials', '**/.env', '**/*.pem']
        for pattern in sensitive_patterns:
            for file_path in Path(layer_extract_path).glob(pattern):
                self.findings.append({
                    'type': 'Sensitive File',
                    'layer': layer_name,
                    'file': str(file_path.relative_to(layer_extract_path))
                })

        # 2. Grep по содержимому
        for root, dirs, files in os.walk(layer_extract_path):
            for file in files:
                full_path = os.path.join(root, file)
                try:
                    with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read(50000)  # Читаем первые 50к символов
                        if self._is_sensitive(content):
                            rel_path = os.path.relpath(full_path, layer_extract_path)
                            self.findings.append({
                                'type': 'Sensitive Content',
                                'layer': layer_name,
                                'file': rel_path,
                                'snippet': content[:100]
                            })
                except:
                    continue  # Пропускаем бинарные файлы

    def _is_sensitive(self, text):
        """Эвристика для определения чувствительной информации."""
        patterns = [
            r'password[=:]\s*[\'"]?\w+',
            r'api[_-]?key[=:]\s*[\'"]?\w+',
            r'bearer\s+\w+',
            r'gh[pus]_[a-zA-Z0-9]{36}',
            r'Ak[aA][0-9A-Z]{16}',
            r'-----BEGIN (RSA|OPENSSH|DSA|EC) PRIVATE KEY-----'
        ]
        for pattern in patterns:
            if re.search(pattern, text, re.IGNORECASE):
                return True
        return False

    def report(self):
        """Выводит отчет."""
        for finding in self.findings:
            print(f"[{finding['type']}] в слое {finding['layer']}: {finding.get('file', 'N/A')}")
            if 'snippet' in finding:
                print(f"   Сниппет: {finding['snippet']}")

    def cleanup(self):
        """Очищает временные файлы."""
        import shutil
        shutil.rmtree(self.temp_dir)

# Использование
if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Использование: python scanner.py <имя_образа>")
        sys.exit(1)

    archaeologist = DockerImageArchaeologist(sys.argv[1])
    try:
        tar_path = archaeologist.pull_and_save()
        archaeologist.extract_and_analyze(tar_path)
        archaeologist.report()
    finally:
        archaeologist.cleanup()

Это база. Ее можно расширять: добавить поддержку приватных реестров, параллельную обработку, интеграцию с базами данных для хранения результатов, более умные эвристики, сравнение diff между тегами одного образа.


Часть 7: Рамки.

Важно. Весь этот инструментарий - обоюдоострое оружие.
  • Не нарушай закон. Сканирование публичных образов на Docker Hub, вероятно, не нарушает их ToS (но прочти их!), однако использование найденных креденшелов для доступа к системам - это уже компьютерное мошенничество в большинстве юрисдикций.
  • Ответственное раскрытие. Если ты найдешь критическую утечку в образе компании, попробуй связаться с ними ответственно. У многих есть страницы для сообщения об уязвимостях (Security.txt, HackerOne, Bugcrowd).
  • Цель - защита. Используй эти знания для аудита своих собственных образов, образов, которые использует твоя компания, и для обучения коллег. Создавай культуру безопасности, а не паранойи.

Вот мы и добрались до конца этой прогулки по слоёным пустошам публичных реестров. Если ты читал это не просто по диагонали, а пропускал через себя, проверяя команды, возможно, даже запускал скрипты на своих же образах - в твоей голове сейчас должна произойти тихая, но фундаментальная перестройка. Ты больше никогда не сможешь смотреть на команду docker pull как на безобидное действие. Ты будешь видеть за ней граф зависимостей, каждый узел которого - это потенциальная капсула времени с цифровым ДНК своего создателя. И это хорошо. Это и есть та самая осознанность, которой так не хватает в нарративе «законтейнеризируй и забудь».

Давай резюмируем не просто выводы, а сдвиг в парадигме, который мы с тобой прошли.

1. Образ - это не состояние, это процесс.
Главный вывод, который переворачивает всё с ног на голову. Мы привыкли думать об образе как о статичном артефакте, «замороженной ВМ». На деле -это протокол сборки, последовательность снимков, каждый из которых хранит не только добавленное, но и контекст того, что было до. Удаление файла - это иллюзия для runtime, но не для исследователя. Поэтому безопасность образа определяется не тем, что в нём есть в финале, а тем, что в него попало на любом этапе сборки. Это меняет точку приложения сил: защищать нужно не столько итоговый образ, сколько процесс его сборки.

2. Инструменты смотрят в прошлое. Мы научились смотреть в историю.
Trivy, Clair, Docker Scan -они прекрасно ищут CVE в libssl. Они борются с известными уязвимостями. Это важно, это гигиена. Но это реактивная защита. Наш подход -проактивный и исследовательский. Мы ищем не уязвимости в библиотеках, а ошибки в процессах людей. Утечка секрета - это не баг в коде, это сбой в workflow разработчика, в настройках CI/CD. Наши инструменты (dive, skopeo, кастомные скрипты) - это не конкуренты сканерам CVE. Это инструменты более высокого, почти социотехнического уровня. Они отвечают на вопрос «как работали люди, создавшие этот артефакт?» и, как следствие, «где в их процессе дыра?».

3. Сложность - не враг, а союзник, если её понять.
Что если «Одна команда -и всё работает»? Мы поступили наоборот: мы разобрали сложность Docker-образа на составляющие (манифесты, слои, диффы, JSON-конфиги) и посмотрели, как каждая из них может стать источником утечки. Это даёт нам не слепое доверие, а обоснованный контроль. Теперь ты знаешь, что:
  • docker history вытягивается из конфига образа и может быть отредактирована лишь частично.
  • Слой - это tar-архив с своими метаданными, который можно анализировать без Docker daemon.
  • ADD http://... может закешировать в слое не только бинарные данные, но и HTTP-заголовки, включая Authorization.
  • Переменная окружения, заданная через ENV в Dockerfile, навсегда остаётся в истории сборки, даже если в финальном образе её переопределили.

4. Это не про взлом, а про понимание уязвимости как системы.
После этого руководства ты начинаешь мыслить не как защитник крепости, а как её проектировщик, который также знает все слабые места кладки. Ты понимаешь, что разработчик, который вставил токен в RUN git clone, не идиот. Он, вероятно, спешил, работал в рамках сломанного процесса (нет доступа к приватным пакетам иначе), или ему просто никто не объяснил последствий. Твоя задача, обладая этим пониманием, -не тыкать его носом в ошибку, а построить систему, которая сделает ошибку невозможной или немедленно обнаружит её. Multi-stage сборки, BuildKit secrets, жёсткий .dockerignore - это не «best practices» из книжки. Это инженерные решения, возникающие напрямую из понимания устройства слоёв.

5. Защита - это не одна серебряная пуля, это многослойная стратегия (снова слои!).
Исходя из всего вышесказанного, выстраивается не список пунктов, а стратегия, где каждый уровень защищает от сбоя на предыдущем:
  • Уровень процесса (люди): Обучение, код-ревью Dockerfile, шаблоны.
  • Уровень сборки (инструменты): Обязательное использование multi-stage, запрет на COPY . ., использование секретов BuildKit, линтеры (hadolint).
  • Уровень контроля версий (исходники): Pre-commit хуки с git-secrets или truffleHog, чтобы секрет даже не попал в репозиторий.
  • Уровень CI/CD (автоматика): Сборка только в чистых, изолированных окружениях; сканирование артефактов перед пушем в реестр (не только на CVE, но и на секреты тем же методом, что и мы); подпись образов (Cosign).
  • Уровень реестра (хранение): Приватные реестры с минимальными правами; политики, запрещающие пушить образы с тегами latest или без диджитальной подписи.
  • Уровень деплоя (запуск): Admission Controllers в Kubernetes, проверяющие происхождение, подпись и отсутствие известных секретов в образе перед запуском.
  • Уровень мониторинга (реагирование): Постфактумное сканирование уже работающих образов в кластере (Falco, сторонние сканеры) и мониторинг логов на предмет попыток использования утекших ключей.
Обрати внимание: наше исследование легло в основу защиты на уровнях 2, 4 и 6. Прямо из атаки выросла оборона.

6. Этичность как продолжение компетентности.
Теперь, когда ты знаешь, как просто найти секреты в публичных образах, возникает искушение. Искушение провести массовое сканирование, собрать «коллекцию» ключей, почувствовать себя повелителем утекших данных. Остановись. Помни: твоя сила -в понимании механизма, а не в злоупотреблении его результатами. Найденный в публичном доступе AWS-ключ -это не подарок судьбы, это чья-то критическая ошибка. Правильный путь - ответственное раскрытие. Не ради награды или благодарности (их часто не будет), а ради того, чтобы экосистема стала чуть менее дырявой. Ради того принципа «солидарности с читателем», который распространяется и на тех самых нерадивых разработчиков - они тоже часть нашего сообщества, просто им не хватило знаний, которые теперь есть у тебя.

Финальный аккорд: что делать прямо сейчас?
  1. Личный аудит. Запусти docker images, выбери свой самый старый или самый странный образ. Проведи над ним весь ритуал: docker save, распаковка, dive, grep по слоям. Уверен на 99%, что найдешь что-то, от чего станет не по себе. Это самый ценный урок.
  2. Документировать и делиться. Напиши в интренал-вики своей компании краткую выжимку из этой статьи. Не как приказ сверху, а как «эй, ребята, посмотрите, какую штуку я узнал, давайте проверим наши пайплайны».
  3. Внедрить одну практику. Выбери ОДНУ вещь: multi-stage для нового сервиса, или .dockerignore в старый проект, или линтер для Dockerfile в CI. Не пытайся изменить всё сразу. Главное - начать.
  4. Сменить перспективу. В следующий раз, когда будешь гуглить проблему и найдешь решение в виде docker run -e KEY=value some-image, остановись. Спроси себя: «А что внутри этого some-image? Откуда он? Можно ли ему доверять?». Это и есть момент перехода от пользователя к технологу.
Контейнеры - не зло. Они великолепная технология, давшая невиданную ранее гибкость. Но, как и любая мощная технология, они требуют уважения и глубокого понимания. Массы выбрали путь упрощения и абстракции, и в этом её ловушка. Мы с тобой выбрали путь исследования. Это путь, на котором нет простых ответов, зато есть надежные системы, построенные на знании, а не на вере.

Держи порох сухим, а слои - чистыми.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab