Безпека Docker: припиніть витік секретів за допомогою цієї структури файлів

Було корисно?

Більшість витоків секретів у Docker не є кіношними зломами. Вони нудні: випадковий .env, запечений в образ, артефакт CI, завантажений «для налагодження», або розробник, який запускає docker inspect і випадково вставляє вивід у задачу.

Вирішити це не потребує нової платформи, міграції у vault або дванадцяти-тижневого комітетного ритуалу. Потрібна структура файлів, яка робить безпечний шлях простим — і робить небезпечний шлях настільки незручним, щоб люди перестали так робити.

Одна структура файлів, яка змінює все

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

cr0x@server:~$ tree -a -L 3 .
.
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── compose.yaml
├── scripts
│   ├── bootstrap-dev.sh
│   └── verify-no-secrets.sh
├── src
│   └── app.py
├── config
│   ├── app.example.yaml
│   └── logging.yaml
└── secrets
    ├── README.md
    ├── dev
    │   ├── app.env.example
    │   └── tls.example
    └── runtime
        ├── DO_NOT_COMMIT
        └── .keep

Що означає кожна директорія (і чого вона забороняє)

  • src/: лише код застосунку. Ніколи секрети. Якщо коду потрібен секрет, він читає його під час виконання з шляху до файлу або з інжектованої змінної оточення. Код не має відправляти секрет у реліз.
  • config/: неконфіденційна конфігурація, закомічена в git. Надавайте шаблони *.example*, щоб інженери не вигадували довільні імена на кшталт prod.env.
  • secrets/: тут криється фішка. Ви створюєте місце для секретів, щоб люди перестали розкидати їх скрізь. Але ви також робите їх неможливими для коміту через політики й інструменти:
    • secrets/dev/ містить лише приклади для локальної розробки. Файли-приклади показують форму, а не вміст.
    • secrets/runtime/ — це місце, куди потрапляють реальні секрети на машині або в робочому просторі пайплайна. Воно ніколи не комітиться. Воно монтується під час виконання.
  • .dockerignore: ваш перший жорсткий бар’єр. Docker може копіювати тільки те, що ви відправляєте в контекст збірки. Не надсилайте секрети.
  • compose.yaml: визначає монтування/секрети під час виконання, а не копіювання під час збірки.
  • scripts/verify-no-secrets.sh: автоматизація, яка руйнує збірку, якщо хтось спробує проявити креативність.

.dockerignore, що виконує важку роботу

Якщо ви візьмете лише одну річ з цієї статті — візьміть це: ваш контекст збірки — це поверхня атаки. Демон Docker (локальний або віддалений) отримує tar вашого контексту. Якщо ваш .env там, ви вже програли.

cr0x@server:~$ cat .dockerignore
# Never send secrets to the build context
secrets/
**/*.pem
**/*.key
**/*id_rsa*
**/*.p12
**/*.jks
**/.env
**/.env.*
**/*credentials*
**/*token*
**/*secret*
**/kubeconfig
**/.npmrc
**/.pypirc
**/.netrc

# Common junk that bloats builds and leaks internals
.git/
.gitignore
Dockerfile*
compose*.yml
compose*.yaml
node_modules/
__pycache__/
*.log

Так, ви все ще можете випадково витекти секрет, якщо ім’я файлу не підпадає під ці шаблони. Ось чому структура також включає сканування й запобіжники. Захист у глибину — це просто гарний спосіб сказати «припустимо, що хтось зрештою зробить щось не так».

Шаблон Dockerfile: збирати без секретів, запускати з секретами

Ваш Dockerfile не повинен приймати секрети як ARG. Він не має копіювати .env. Він не має робити RUN echo "$TOKEN". Якщо секрети потрібні під час збірки (приватні реєстри залежностей), використовуйте BuildKit secret mounts, щоб секрет ніколи не став шаром.

cr0x@server:~$ sed -n '1,160p' Dockerfile
# syntax=docker/dockerfile:1.6
FROM python:3.12-slim AS runtime

WORKDIR /app

# Install deps first to leverage caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy only the application
COPY src/ ./src/
COPY config/ ./config/

# Run as non-root
RUN useradd -r -u 10001 appuser && chown -R appuser:appuser /app
USER appuser

# Secrets are loaded at runtime from /run/secrets or mounted files
ENV APP_CONFIG=/app/config/logging.yaml
CMD ["python", "-m", "src.app"]

Шаблон Compose: інжекція під час виконання, а не випікання під час збірки

Compose — це місце, де люди стають ліниві, бо «це лише дев». А потім дев переходить у стейджинг, стейджинг — у прод, і прод — у інцидентну рев’ю.

cr0x@server:~$ sed -n '1,200p' compose.yaml
services:
  app:
    build:
      context: .
    image: acme/app:dev
    environment:
      # Non-secret values only
      - LOG_LEVEL=info
    volumes:
      # Runtime config is fine if it is not secret
      - ./config:/app/config:ro
      # Real secrets: mounted from secrets/runtime (not in git)
      - ./secrets/runtime:/run/secrets:ro
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    ports:
      - "8080:8080"

Рішення: якщо ваша команда покладається на файли .env, збережіть їх — але під secrets/runtime і монтуйте тільки для читання. Мета — не допустити потрапляння їх в історію git або шари Docker.

Жарт 1: Якщо ви покладете секрети в змінні оточення, вони рано чи пізно опиняться на скриншоті. Люди роблять скриншоти, ніби це стратегія бекапу.

Чому секрети витікають у Docker (режими відмов, які ви можете відтворити)

Docker витікає, бо він добре переносить байти і погано розуміє наміри. Він радо запаковує ваш контекст у tar, кешує шари назавжди і записує метадані, про які ви не подумали. Тим часом інженери оптимізують під «працює зараз», бо on-call — це досвід, який виховує характер, але ніхто його не просив.

Шлях витоку №1: контекст збірки включає секрети

docker build відправляє весь каталог контексту демон. Якщо демон віддалений (поширено в CI або при використанні спільного билдера), ви щойно відправили секрети по мережі. Навіть якщо Dockerfile їх не копіює, передача контексту може логуватись, кешуватись або бути доступною для перегляду у непередбачуваний спосіб.

Шлях витоку №2: шари Dockerfile зберігають історію

Коли ви робите RUN export TOKEN=... && some-command або RUN echo "$TOKEN" > /tmp/token, ви створюєте шар. Навіть якщо потім видалите файл, шар може його зберегти. Історія образу також може показати аргументи збірки та рядки команд.

Шлях витоку №3: змінні оточення доступні для виявлення

Змінні оточення показуються в docker inspect. Вони можуть з’являтися в дампах аварій, звітах підтримки, списках процесів і логах. Їх також зазвичай додають у метадані моніторингу («для зручності»), через що ви отримуєте ключ API у бекенді метрик.

Шлях витоку №4: неправильні монтажі і права

Монтування файлу з секретом — це нормально. Монтування його записуваним і запуск контейнера як root — це шлях до мутованих секретів, випадкових комітів і 2:00 ранку дебаг-сесій «чому контейнер перезаписав мою конфігурацію?»

Шлях витоку №5: артефакти CI і логи для налагодження

Системи CI люблять артефакти. Люди люблять артефакти для налагодження. Якщо ваш пайплайн завантажує docker inspect, вивід env або весь tar робочого простору, секрети втечуть. Не тому, що хтось злий — тому, що хтось втомився.

Декілька фактів і історичний контекст (бо наші помилки старі)

  • Факт 1: Модель шарів Docker базується на union-файлових системах; видалення файлу в пізнішому шарі не прибирає його з ранніших шарів. Ось чому «я видалив секрет пізніше» — не виправлення.
  • Факт 2: Змінні оточення були стандартним механізмом конфігурації з ранніх Unix. Вони зручні — і історично погані для конфіденційності.
  • Факт 3: Початковий процес збірки Docker відправляв весь контекст як tar-стрім до демона. Ця поведінка спричинила роки випадкового витоку секретів у CI-білдерах.
  • Факт 4: BuildKit ввів секретні монтування саме тому, що люди продовжували пхати креденшали в build args і шари образу для доступу до приватних реєстрів пакетів.
  • Факт 5: docker history може розкрити команди, використані для збірки образу. Якщо секрет з’являється в RUN-рядку, він може бути видимим, навіть якщо файлу немає.
  • Факт 6: Багато рантаймів контейнерів і оркестраторів зберігають змінні оточення в стор-ах метаданих (іноді в логах), що множить радіус ураження «просто покласти в ENV».
  • Факт 7: Ранні підходи до контейнерів заохочували «один артефакт» — запекти все в образ. Практика безпеки пішла іншим шляхом: незмінні образи, змінні секрети інжектуються під час виконання.
  • Факт 8: Дизайн Git ускладнює видалення секретів: навіть якщо ви видалите файл, він залишається в історії, поки ви не перепишете її. Найкращий витік — той, що ви ніколи не комітили.
  • Факт 9: Багато гучних порушень починалися з виявлення облікових даних у репозиторіях або артефактах, а не з якоїсь нової експлойт-ланцюжка. Зломщики обожнюють розкопки.

Правила: де секрети можуть жити, а де ніколи не можуть

Що робити

  • Тримайте секрети поза контекстом збірки. Використовуйте .dockerignore агресивно і ставтеся до нього як до контролю безпеки, а не оптимізації продуктивності.
  • Інжектуйте секрети під час виконання через змонтовані файли. Віддавайте перевагу /run/secrets (звичний шлях) або монтуванню лише для читання під виділеною директорією.
  • Використовуйте BuildKit secret mounts для автентифікації під час збірки. Це найменш поганий спосіб діставати приватні залежності без витоку креденшалів у шари.
  • Зробіть «безпечне» макетування за замовчуванням. Розробники слідують шляхові найменшого опору; ваш репо має робити безпечний шлях найкоротшим.
  • Скануйте постійно. Не довіряйте людям. І не довіряйте собі шість місяців тому.

Чого не робити

  • Ніяких секретів у ENV або в Compose environment. Якщо це секрет — він має бути у файлі або в інтеграції зі сховищем секретів, а не в метаданих.
  • Не копіюйте файли з секретами в Dockerfile. Навіть «на секунду». Шари — назавжди.
  • Жодного «debug output», що друкує env. Якщо ваш скрипт налагодження починається з env | sort, видаліть його і попросіть вибачення у майбутнього.
  • Не комітьте реальні секрети, навіть «лише для тестування». Тестування — це шлях, яким витоки стають постійними.

Цитата, що триматиме вас чесними

Парафразована ідея від Gene Kim (автор з DevOps/операцій): «Найкращий спосіб підвищити надійність — робити проблеми видимими і виправляти їх системно.»

Практичні завдання: 14 команд для аудиту і виправлення витоків

Це завдання рівня продакшн. Кожне включає: команду, що означає вивід і яке рішення приймати. Запускайте їх на машині розробника, в CI і на білд-сервері. Різні середовища витікають по-різному.

Завдання 1: Підтвердити, що контекст збірки не відправляє секрети

cr0x@server:~$ docker build --no-cache --progress=plain -t acme/app:check .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 612B done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load metadata for docker.io/library/python:3.12-slim
#4 [internal] load build context
#4 transferring context: 48.35kB 0.0s done
...

Значення виводу: «transferring context» показує розмір, відправлений демону. Якщо ви бачите мегабайти, яких не очікували, ймовірно ви відправляєте сміття або секрети.

Рішення: Якщо контекст великий або підозрілий — посильніше налаштуйте .dockerignore і перезапустіть, поки розмір контексту не відповідатиме тому, що ви б були готові надіслати незнайомцю електронною поштою.

Завдання 2: Перевірити, що .dockerignore реально застосовується

cr0x@server:~$ docker build -t acme/app:ignore-test --no-cache --progress=plain .
#4 [internal] load build context
#4 transferring context: 48.35kB done
#4 DONE 0.1s

Значення виводу: Якщо тимчасово помістити великий файл у secrets/runtime і розмір контексту не змінюється, ваші правила ігнорування працюють. Якщо він зростає — ні.

Рішення: Виправте шаблони, поки контекст не залишатиметься стабільним навіть коли в secrets/ є реальні файли.

Завдання 3: Просканувати репозиторій на пошук загальних імен файлів зі секретами

cr0x@server:~$ find . -maxdepth 4 -type f \( -name ".env" -o -name ".env.*" -o -name "*.pem" -o -name "*.key" -o -name "*kubeconfig*" \) -print
./secrets/dev/app.env.example

Значення виводу: Все, що знаходиться поза secrets/dev і виглядає як секрет — проблема, що чекає на коміт.

Рішення: Перемістіть файли, схожі на реальні секрети, у secrets/runtime і переконайтеся, що вони ігноруються git та Docker.

Завдання 4: Переконатися, що git не прийме секрети під secrets/runtime

cr0x@server:~$ cat .gitignore
# runtime secrets must never be committed
secrets/runtime/*
!secrets/runtime/.keep

# local env files
.env
.env.*

Значення виводу: Лінія-виняток зберігає файл-заповнювач, щоб директорія існувала. Все інше ігнорується.

Рішення: Якщо secrets/runtime не ігнорується — виправте зараз; інакше хтось випадково закомить під час термінового хотфікса.

Завдання 5: Виявити вже відстежені секрети (пастка «ігнорується, але закомічено»)

cr0x@server:~$ git ls-files | grep -E '(^|/)\.env(\.|$)|secrets/runtime|\.pem$|\.key$' || true

Значення виводу: Якщо щось виводиться — це вже в історії git або відстежується в індексі.

Рішення: Видаліть з індексу і ротируйте креденшали. Ігнорування не анулює вже здійснений витік.

Завдання 6: Видалити випадково відстежені файли (не видаляючи локальні копії)

cr0x@server:~$ git rm --cached -r secrets/runtime
fatal: pathspec 'secrets/runtime' did not match any files

Значення виводу: «did not match» — добре: нічого там не відстежується. Якщо команда видаляє файли, у вас була проблема.

Рішення: Якщо файли було видалено, закомітіть видалення і негайно ротируйте будь-які секрети, які там були.

Завдання 7: Переглянути історію образу на предмет витоків через аргументи або команди

cr0x@server:~$ docker history --no-trunc acme/app:check | head
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
a1b2c3d4e5f6   2 minutes ago    CMD ["python" "-m" "src.app"]                   0B        buildkit.dockerfile.v0
<missing>      2 minutes ago    ENV APP_CONFIG=/app/config/logging.yaml        0B        buildkit.dockerfile.v0
<missing>      3 minutes ago    RUN /bin/sh -c useradd -r -u 10001 appuser...  1.2MB     buildkit.dockerfile.v0

Значення виводу: Шукайте токени, паролі, приватні URL або echo-команди, які записували секрети.

Рішення: Якщо щось схоже на секрет з’явилося — перебудуйте з виправленим Dockerfile і відкличте/поміняйте креденшали. Також по можливості очистіть старі теги в реєстрі.

Завдання 8: Перевірити файлову систему образу на «ой» файли

cr0x@server:~$ docker run --rm acme/app:check sh -lc 'ls -la /run /run/secrets || true; find /app -maxdepth 3 -type f -name ".env" -o -name "*.pem" -o -name "*.key" 2>/dev/null || true'
ls: /run/secrets: No such file or directory

Значення виводу: У образі /run/secrets зазвичай не існує, якщо його не створено. Це нормально. Ненормально — знайти .env, ключі або сертифікати в образі.

Рішення: Якщо секретні файли є в образі — вважайте їх скомпрометованими і перебудуйте чисто.

Завдання 9: Переконатися, що контейнери не працюють із секретними змінними оточення

cr0x@server:~$ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $(docker ps -q --filter name=app) | grep -Ei 'pass|token|secret|key' || true

Значення виводу: Нульовий вивід — мета. Якщо ви бачите змінні, схожі на секрети, ви експонуєте їх через інспекцію і, можливо, логи.

Рішення: Перенесіть ці секрети в змонтовані файли або секрети оркестратора і очистіть конфігурацію середовища.

Завдання 10: Перевірити, чи змонтовані файли секретів існують і доступні лише для читання

cr0x@server:~$ docker exec -it $(docker ps -q --filter name=app) sh -lc 'mount | grep /run/secrets; ls -la /run/secrets'
/dev/sda1 on /run/secrets type ext4 (ro,relatime)
total 8
drwxr-xr-x 2 root root 4096 Feb  4 10:22 .
drwxr-xr-x 1 root root 4096 Feb  4 10:22 ..
-r--r----- 1 root root   64 Feb  4 10:22 db_password

Значення виводу: Монтування показує (ro,...) і файл секрету не читається всіма.

Рішення: Якщо воно rw або права надто вільні — виправте Compose/Kubernetes маніфести. Секрети мають бути читабельні тільки для користувача застосунку, а не для всього контейнера.

Завдання 11: Перевірити власність файлу та користувача контейнера (діагностика невідповідності прав)

cr0x@server:~$ docker exec -it $(docker ps -q --filter name=app) sh -lc 'id; stat -c "%U %G %a %n" /run/secrets/db_password'
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)
root root 440 /run/secrets/db_password

Значення виводу: Якщо контейнер працює як appuser, але файл належить root з режимом 440, ваш застосунок може не мати доступу, якщо групові права не збігаються.

Рішення: Або запускайте застосунок з відповідним груповим власником, використовуйте 0444 для лише читання там, де це прийнятно, або налаштуйте проекцію секретів в оркестраторі з правильним UID/GID.

Завдання 12: Просканувати образ на рядки з високою ентропією (дешевий детектор витоку секретів)

cr0x@server:~$ docker save acme/app:check | tar -xOf - | strings | grep -E '[A-Za-z0-9+/]{32,}={0,2}' | head

Значення виводу: Це шумить, але ловить base64-подібні блоби, які іноді вказують на вбудовані токени.

Рішення: Якщо ви бачите щось підозріле — звузьте пошук, експортуючи файлову систему і грепаючи конкретні шляхи. Ставтеся до позитивів серйозно.

Завдання 13: Правильне використання BuildKit secret mount (приклад приватної залежності)

cr0x@server:~$ DOCKER_BUILDKIT=1 docker build \
  --secret id=pypi_token,src=./secrets/runtime/pypi_token \
  --progress=plain -t acme/app:with-private-deps .
#6 [runtime 2/6] RUN --mount=type=secret,id=pypi_token ...
#6 DONE 8.7s

Значення виводу: Ви бачите крок RUN --mount=type=secret. Це означає, що ви не копіюєте токен в образ.

Рішення: Якщо Dockerfile використовує ARG замість цього — зупиніться і рефакторіть. Build args не для зберігання секретів.

Завдання 14: Доказати, що секрет не потрапив у фінальний образ

cr0x@server:~$ docker run --rm acme/app:with-private-deps sh -lc 'grep -R "pypi" -n / 2>/dev/null | head'

Значення виводу: Ви шукаєте рядки токенів або файли конфігурації автентифікації. Ідеально — нічого змістовного (можуть бути незначущі збіги; розслідуйте їх).

Рішення: Якщо токен або конфіг існує у фінальній файловій системі — крок витік. Ротируйте і виправляйте Dockerfile.

Три корпоративні міні-історії з землі «в мене на лаптопі працювало»

Міні-історія 1: Інцидент через неправильне припущення

Команда, що працює з платежами, довго збирала образ без проблем. Вони використовували приватний реєстр пакетів і вважали, що аргумент збірки «достатньо безпечний», бо його передавали тільки в CI. Dockerfile приймав ARG NPM_TOKEN, записував його у ~/.npmrc, встановлював залежності, а потім видаляв файл. Чисто, так?

Не чисто. Токен уже жив у шарі. Пізніше внутрішня програма сканування образів зберегла образи в спільне сховище артефактів для аналізу. Інша команда — щоб налагодити збірку — витягла артефакт і розпакувала tar. Той токен не був «в проді», але це була дійсна облікова інформація.

Радіус ураження був незручним, але не катастрофічним: доступ до внутрішніх пакетів, незначний ризик ланцюга постачання і тиждень примусової ротації по проєктах. Справжня шкода була моральна. Інженери, які були обережні з «не комітити секрети», дізналися, що шари Docker — це ще одна історія.

Виправлення було нудним: BuildKit secret mounts, строгий .dockerignore і політика, що токени не з’являються в інструкціях Dockerfile. Команда також перестала завантажувати сирі образи як «артефакти налагодження». Вони завантажували журнали зі скраберами і лише необхідні метадані.

Після того, як усе вляглося, хтось запитав, чому це не виявили раніше. Відповідь проста: всі вважали, що «видалив файл» означає «він зник». Це припущення і підводить вас шарами.

Міні-історія 2: Оптимізація, яка відбилася боком

Група платформи оптимізувала час CI-збірок, ввімкнувши агресивне кешування шарів Docker на спільних раннерах. Збірки стали швидшими, всі були задоволені пару спринтів. Потім почалися «примарні» поведінки: фіче-бранч міг збиратися успішно навіть після видалення доступу до секрету, бо шар, що використовував секрет, був у кеші.

Гірше: розробник додав тимчасовий рядок для налагодження, який друкував значення конфігурації, що випадково містило токен. Логи збірки зберігалися довше, ніж потрібно. Тим часом кешовані шари, що містили конфіги для завантаження залежностей, ділилися між проєктами. Так не повинно бути, але «не повинно» — не контроль доступу.

Інцидент не був класичним зломом. Це була експозиція. Безпека позначила це як «помилка обробки креденшалів». Платформна команда — як «помилка налаштування скопу кешу». Усі погодилися: корінь — оптимізація, яка знизила тертя для збірок і підняла тертя для ізоляції.

Вони лишили кешування — але дисципліновано: ізольовані кеші для проєктів, ніякого крос-тенантного шарингу і явні правила, що кроки зі секретами не кешуються або використовують secret mounts, які не заражають шари. Також скоротили зберігання логів і скрабили відомі патерни ключів.

Продуктивність — це фіча. Безпека — теж фіча. Якщо ви оптимізуєте одну, тихо забираючи в іншої — згодом заплатите відсотки.

Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію

Компанія в сфері охорони здоров’я мала простий стандарт: у кожному сервісному репо є secrets/runtime, ігнорований git і Docker, а пайплайни монтують його під час виконання. Без винятків. Інженери жалілися місяць. Потім перестали помічати це.

Одного п’ятничного дня хтось випадково скопіював продакшн-креденшал у локальний файл prod.env в корені репозиторія. Розробник збирався комітити. pre-commit хук завив. CI теж завив. Контекст збірки залишився малим, бо .dockerignore ігнорував патерни .env*, і репозиторний сканер секретів позначив високенно-ентропійний бінар.

Креденшал ніколи не потрапив у git. Він ніколи не потрапив у образ. Він ніколи не потрапив у реєстр. Люди все одно ротирували його, бо були навчені ставитися до «майже промаху» як до «можливо компрометовано». Ротація зайняла хвилини, не дні, бо була документація і послідовний шлях інжекції секретів.

Безпека не мусила грати детектива. SRE не мусив будити нікого. Система не врятувала день хитрою криптою; вона врятувала його передбачуваним розміщенням файлів і кількома невеликими запобіжниками.

Жарт 2: Найкращий секрет — той, який ви ніколи не розкрили. Другий найкращий — той, який ви ротируєте, перш ніж хтось помітить.

Швидкий план діагностики (перевірити перше/друге/третє)

Коли кажуть «ми витекли секрет» або «сканер каже, що образ містить креденшали», не сперечайтеся про теорію. Тріажуйте швидко. Ось план, який я використовую.

Перше: підтвердити, чи секрет у git-історії, в образі або тільки під час виконання

  • Перевірте відстеження git: git ls-files + grep-патерни; якщо відстежується — вважайте компрометацією.
  • Перевірте історію образу: docker history --no-trunc на видимі рядки команд.
  • Перевірте файлову систему: запустіть контейнер і find для типових файлів секретів.

Чому перше: відновлення різниться. Git-історія — постійна, якщо не переписати. Реєстри образів реплікують. Експозиція тільки під час виконання може обмежуватися одним хостом.

Друге: ідентифікувати механізм інжекції

  • Змінні оточення: docker inspect і перевірка маніфестів оркестрації.
  • Змонтовані файли: перевірте монтування /run/secrets і права.
  • Секрети під час збірки: знайдіть ARG, --build-arg і файли автентифікації пакетних менеджерів (.npmrc, .netrc).

Чому друге: потрібно запобігти повторенню. Ротація секрету — це крок нуль; виправлення шляху — реальна робота.

Третє: оціни радіус ураження і ротируй рішуче

  • Шукайте строки секрету в логах CI і в артефактах.
  • Перевірте реєстри на уражені теги/дайджести; видаліть або помістіть під карантин.
  • Ротируйте креденшали; інвалідизуйте токени; перевидайте ключі; оновіть деплойменти з новим шляхом секретів.

Чому третє: секрети витікають швидше, ніж ви встигнете намітити зустріч. Ротація краще за аналіз-параліч.

Звичайні помилки: симптом → корінна причина → виправлення

Помилка 1: «Ми видалили файл у пізнішому кроці Dockerfile»

Симптоми: сканер секретів позначає образ; ви не знаходите файл у запущеному контейнері; безпека наполягає, що він усе ще там.

Корінна причина: секрет існував у ранньому шарі; видалення лише замаскувало його у фінальному вигляді файлової системи.

Виправлення: перебудуйте, ніколи не записуючи секрет у шар. Використовуйте BuildKit secret mounts або діставайте залежності поза збіркою образу. Очистіть старі теги образів і ротируйте креденшали.

Помилка 2: «Це нормально, це лише змінні оточення»

Симптоми: секрети з’являються в docker inspect; бандли підтримки містять токени; хтось вставив конфіг у чат і тепер це в пошуку.

Корінна причина: змінні оточення — метадані; їх копіюють, логують, скраблять і інспектують.

Виправлення: монтуйте секрети як файли під /run/secrets (або проектовані секрети оркестратора). Залишайте змінні оточення для небезпечних перемикачів. Якщо застосунок підтримує лише змінні оточення, обгорніть його: читайте з файлу і експортуйте в маленькому entrypoint, але приймайте залишковий ризик і обмежуйте доступ до інспекції/логів.

Помилка 3: «У нас є .dockerignore, то ми в безпеці»

Симптоми: передача контексту CI велика; секрети знаходять у кеші білдера; різні машини дають різні результати.

Корінна причина: .dockerignore неповний, або ви збираєте з іншого контексту, ніж думаєте (монорепозиторії, неправильний каталог, відмінності шляхів в CI).

Виправлення: перевірте розмір контексту в логах збірки; явно вкажіть context: в Compose; додайте тести, які падають, якщо secrets/ є в контексті; стандартизуйте точки входу збірки (скрипти), щоб розробники не робили збірки вручну.

Помилка 4: «Ми монтуємо секрети, але застосунок не може їх читати»

Симптоми: застосунок падає з permission denied; файли секретів є, але читання не вдається; інженери «виправляють» запуском як root.

Корінна причина: невідповідність UID/GID і надто суворий режим файлу, або проекція секретів з root-only правами.

Виправлення: запускайте застосунок з фіксованим UID; проектуйте секрети з правильною власністю; використовуйте групово-читабельні режими; уникайте «запуск як root» як заплатки.

Помилка 5: «Ми змонтували ./secrets у контейнер і забули, що воно писабельне»

Симптоми: файли секретів змінюються несподівано; локальні дев-секрети перезаписуються; хтось випадково комітить змінені файли.

Корінна причина: записуваний bind-mount з хоста; процеси в контейнері записують назад у файлову систему хоста.

Виправлення: монтуйте секрети лише для читання. Робіть кореневу файлову систему контейнера read_only: true і дозволяйте запис лише на tmpfs.

Помилка 6: «CI завантажив артефакти для налагодження, включаючи робочий простір»

Симптоми: секрет з’являється в сховищі артефактів CI; рядок токена знайдений у заархівованих tarball; безпека питає, чому ваш пайплайн — це сервіс ексфільтрації даних.

Корінна причина: занадто широкі правила завантаження артефактів; відсутній скраббер; немає розділення між виходом збірки і станом робочого простору.

Виправлення: завантажуйте тільки явні виходи збірки; ніколи не завантажуйте docker inspect або повний робочий простір; скрабуйте логи; взагалі тримайте секрети поза робочим простором за допомогою описаної структури.

Чеклісти / покроковий план

Покроково: прийняти структуру файлів в існуючому репо

  1. Створіть директорії: config/, secrets/dev/, secrets/runtime/, scripts/.
  2. Перемістіть неконфіденційні конфіги з кореня репозиторію в config/. Тримайте закомічені конфіги не секретними.
  3. Створіть шаблони секретів в secrets/dev (наприклад, app.env.example) і задокументуйте очікувані ключі.
  4. Додайте строгий .gitignore для secrets/runtime і .env*.
  5. Додайте строгий .dockerignore щоб виключити секрети, git та CI-сміття.
  6. Рефакторіть Dockerfile щоб копіювати лише src/ і config/. Видаліть будь-який COPY . ., якщо ви не любите аудити.
  7. Перейдіть на монтування під час виконання в Compose/Kubernetes. Стандартизуйте шлях на /run/secrets.
  8. Припиніть використовувати аргументи збірки для секретів. Замініть їх на BuildKit secret mounts, коли неминуче.
  9. Додайте запобіжники: скрипт, що сканує на секрети і руйнує CI. Зробіть його достатньо швидким для запуску в кожному PR.
  10. Ротируйте креденшали якщо знайдете щось підозріле в історії, образах, реєстрах або логах.

Чекліст CI (мінімальний набір безпеки)

  • Будуйте з увімкненим BuildKit (DOCKER_BUILDKIT=1).
  • Не друкуйте змінні оточення в логах.
  • Не завантажуйте архіви робочого простору як артефакти.
  • Не діліться кешем шарів Docker між проєктами/тенантами, якщо це не спроектовано і промодеровано.
  • Запускайте скан на секрети проти git-diff і проти зібраного образу.

Чекліст runtime (контейнери в продакшн)

  • Запускайте як не-root з фіксованим UID.
  • Монтуйте секрети лише для читання; проектуйте їх з коректними правами.
  • Робіть кореневу файлову систему лише для читання; використовуйте tmpfs для тимчасових записів.
  • Відкидайте Linux capabilities і встановлюйте no-new-privileges.
  • Переконайтеся, що логи ніколи не містять вмісту секретних файлів або дампів оточення.

FAQ

1) Чи файл .env завжди погана ідея?

Ні. .env — зручний формат. Погана ідея — дозволяти йому дрейфувати в git, контексти збірки Docker, образи або артефакти. Тримайте реальні файли в secrets/runtime і ігноруйте їх.

2) Чому файли на монтажі замість змінних оточення?

Файли на монтажі менш імовірно будуть залоговані, скраблені або експоновані через інспекцію метаданих. Вони також підтримують жорсткіші файлові права. ENV — прості, але простота ≠ безпека.

3) А що з підтримкою «secrets» у Docker Compose?

Використовуйте її, де підтримується, але не вважайте магією. Багато команд усе ще бінд-монтують файли секретів. Структура підходить обом підходам: стабільне місце на хості або в CI, де зберігають секрети і монтують під час виконання.

4) Чи можуть BuildKit секрети все одно витекти?

Так, якщо ви їх друкуєте, записуєте на диск або копіюєте в образ. BuildKit дає безпечний механізм інжекції; він не виправляє погані скрипти.

5) Нам потрібен приватний реєстр залежностей під час збірки. Який найбезпечніший патерн?

Використовуйте BuildKit --secret монтування і налаштуйте пакетний менеджер читати креденшали з монтування на час встановлення. Залишайте крок мінімальним і уникайте кешування, якщо не можете довести його чистоту.

6) Якщо секрет закомічено, але репо приватне — чи все одно ротувати?

Так. «Приватне репо» не є границею безпеки, яку можна ідеально перевірити. Ротируйте, потім очищайте історію, якщо цього вимагає політика. Ротація — нагальне завдання.

7) Як завадити розробникам обходити структуру?

Зробіть її дефолтною в шаблонах, додайте pre-commit і CI-перевірки, і контролюйте .dockerignore та патерни Dockerfile в рев’ю. Також: тримайте робочий процес швидким, щоб люди не вигадували «тимчасові» хаки.

8) А Kubernetes?

Ті самі принципи: секрети інжектуються під час виконання, бажано як файли (secret volumes). Тримайте образ вільним від секретів. Стандартизуйте шлях, наприклад /run/secrets, щоб застосунок не думав, звідки секрет прийшов.

9) Чи варті системи лише для читання клопоту?

Так. Вони запобігають класу випадковостей: записам секретів у файлову систему контейнера, мутуванню конфігів і залишенню крихт креденшалів у записуваних шарах. Поєднуйте з tmpfs для /tmp і тим, що потрібно застосунку.

10) Який найшвидший спосіб дізнатися, чи витікаємо через контекст збірки?

Запустіть docker build --progress=plain і дивіться на розмір передачі контексту. Якщо він великий або змінюється, коли ви додаєте файл під secrets/, ваші правила ігнорування неправильні.

Наступні кроки (зробіть це цього тижня)

Секрети не витікають через те, що інженери неуважні. Вони витікають, бо системи дозволяють і робочі процеси заохочують швидкість. Виправте робочий процес.

  1. Прийміть структуру: src/, config/, secrets/dev (приклади), secrets/runtime (реальні, ігноровані), строгий .dockerignore.
  2. Рефакторіть Dockerfile, щоб копіювати тільки потрібне. Видаліть COPY . ., якщо не можете обгрунтувати письмово.
  3. Перемістіть секрети в runtime-монтування під /run/secrets, лише для читання, з адекватними правами і не-root контейнерами.
  4. Увімкніть BuildKit і використовуйте secret mounts для автентифікації під час збірки. Забороніть ARG для креденшалів.
  5. Додайте дві дешеві запобіжні міри: сканування репозиторію на файли, що нагадують секрети, і сканування зібраних образів на вміст, що нагадує секрети.
  6. Якщо знайдете щось: спершу ротируйте, потім аналізуйте — і не торгуйтеся з власним передбаченням помилок.
← Попередня
Windows показує «Підключено, немає інтернету»? Виправте без скидання всього
Наступна →
Встановлення RHEL 10: корпоративний чекліст налаштування, який пригодився б раніше

Залишити коментар