Освоение переменных окружения в Docker: Конфигурация против секретов

Откройте для себя безопасные и гибкие развертывания Docker, освоив переменные окружения. Это подробное руководство проясняет критическое различие между использованием переменных окружения для общей конфигурации приложения и безопасным управлением конфиденциальными данными, такими как ключи API и пароли. Изучите практические методы передачи нечувствительных настроек, поймите серьезные риски раскрытия секретов через переменные окружения и узнайте, как использовать Docker Secrets и Compose для надежного, зашифрованного управления секретами. Повысьте свои знания Docker и защитите свои приложения.

Освоение переменных окружения в Docker: Конфигурация против секретов

Переменные окружения удобны в Docker, поскольку позволяют одному и тому же образу работать в средах разработки, тестирования и производства с разными настройками. Это удобство становится рискованным, когда команды помещают пароли, ключи подписи и токены API в ту же корзину, что и уровни логирования и номера портов.

Чистая ментальная модель проста: переменные окружения подходят для нечувствительной конфигурации времени выполнения. Секреты должны поступать из хранилища секретов или смонтированного файла секретов с ограниченным доступом и планом ротации.

Понимание переменных окружения для конфигурации

Переменные окружения — это простой и широко используемый метод передачи конфигурации времени выполнения приложениям, включая те, что работают в контейнерах Docker. Они позволяют изменять поведение приложения без пересборки образа Docker, делая ваши контейнеры более гибкими и переносимыми. Это идеально подходит для нечувствительных, динамических настроек, таких как номера портов приложения, флаги отладки или URL-адреса сторонних сервисов.

Методы передачи переменных конфигурации

Docker предоставляет несколько способов определения и внедрения переменных окружения в ваши контейнеры:

1. Инструкция ENV в Dockerfile

Инструкция ENV устанавливает переменную окружения по умолчанию, которая будет доступна внутри контейнера при его запуске. Это подходит для переменных, которые вряд ли изменятся, или для предоставления разумных значений по умолчанию для вашего приложения.

FROM alpine:latest

ENV APP_PORT=8080
ENV DEBUG_MODE=false

COPY ./app /app
WORKDIR /app
CMD ["/app/start.sh"]

Совет: Хотя ENV устанавливает значения по умолчанию, их можно переопределить во время выполнения.

2. Флаг -e или --env с docker run

При запуске одного контейнера вы можете использовать флаг -e или --env для прямой передачи переменных окружения. Это распространено для ad-hoc тестирования или для предоставления определенных настроек, отличающихся от значений по умолчанию в Dockerfile.

docker run -d -p 80:8080 --name my_app_instance \
  -e APP_PORT=80 \
  -e DEBUG_MODE=true \
  my_app_image:latest

3. env_file в Docker Compose

Для управления несколькими переменными окружения, особенно в нескольких сервисах, определенных в файле docker-compose.yml, опция env_file очень удобна. Она позволяет загружать переменные из одного или нескольких файлов .env, сохраняя ваш docker-compose.yml более чистым.

docker-compose.yml:

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    env_file:
      - ./config/app.env

./config/app.env:

APP_PORT=8080
DEBUG_MODE=false
API_ENDPOINT=https://api.example.com/v1

4. Ключ environment в Docker Compose

В качестве альтернативы вы можете определить переменные окружения непосредственно в разделе environment сервиса в docker-compose.yml. Это часто предпочтительно для небольшого количества переменных или для переменных, специфичных для одного сервиса.

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    environment:
      APP_PORT: 8080
      DEBUG_MODE: false

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

Хотя переменные окружения отлично подходят для конфигурации, они принципиально небезопасны для управления конфиденциальными данными (секретами), такими как пароли баз данных, ключи API или частные ключи SSH. Это критическая уязвимость безопасности, которую часто упускают из виду, особенно в средах разработки.

Почему переменные окружения небезопасны для секретов:

  1. Видимость через docker inspect: Любой, у кого есть доступ к хосту Docker, может легко просмотреть переменные окружения работающего контейнера с помощью docker inspect <container_id>. Это означает, что ваши секреты видны в открытом виде.

    # Пример раскрытия секрета (НЕ ДЕЛАЙТЕ ЭТОГО В ПРОДАКШЕНЕ)
    docker run -d -e DB_PASSWORD=mysecretpassword --name insecure_app nginx:latest
    
    # Любой может увидеть пароль
    docker inspect insecure_app | grep DB_PASSWORD
    
  2. Подглядывание за процессами: Внутри контейнера другие процессы или пользователи (если их несколько) могут читать переменные окружения, особенно если приложение работает от root или имеет повышенные привилегии.

  3. Логирование и история: Переменные окружения могут случайно попасть в логи, историю CI/CD конвейера или историю оболочки, что приведет к случайному раскрытию.

  4. Слои образа: Если вы используете ENV в Dockerfile с секретом, этот секрет встраивается в слой образа и остается там, даже если вы попытаетесь unset его в более позднем слое. Это делает секрет извлекаемым из самого образа.

  5. Случайное распространение: Файлы .env или docker-compose.yml, содержащие секреты, часто попадают в системы контроля версий или распространяются ненадлежащим образом, что приводит к широкому раскрытию.

Предупреждение: Относиться к конфиденциальной информации как к обычным переменным окружения — распространенная ошибка безопасности. Всегда предполагайте, что переменные окружения общедоступны на хосте и внутри контейнера.

Безопасное управление секретами в Docker

Чтобы устранить недостатки безопасности переменных окружения для конфиденциальных данных, Docker предоставляет специальные возможности управления секретами, в первую очередь через Docker Secrets (для Docker Swarm) и внешние инструменты, такие как Docker Compose с функциональностью secrets (которая может использовать секреты Docker Swarm или просто монтировать файлы).

Docker Secrets (режим Docker Swarm)

Docker Secrets — это функция, интегрированная с режимом Docker Swarm, которая обеспечивает безопасный способ передачи и хранения конфиденциальных данных для сервисов. Секреты:

  • Зашифрованы в покое в журналах Raft менеджера Swarm.
  • Передаются безопасно авторизованным задачам сервиса.
  • Монтируются как файлы в памяти в файловой системе контейнера, обычно по пути /run/secrets/<secret_name>, а не раскрываются как переменные окружения.
  • Доступны только сервисам, которым явно предоставлен доступ.

Как использовать Docker Secrets (режим Swarm)

  1. Инициализируйте Swarm (если еще не сделано):
    
    

docker swarm init ```

  1. Создайте секрет: Секреты создаются из файла или стандартного ввода.
    
    

echo "my_secure_db_password" | docker secret create db_password_secret - echo "SG.your_api_key_here" | docker secret create sendgrid_api_key - ```

  1. Разверните сервис с секретом: Сервисы ссылаются на секреты по имени. Docker монтирует секрет в контейнер.
    
    

docker service create --name my-webapp
--secret db_password_secret
--secret sendgrid_api_key
my_app_image:latest ```

  1. Доступ к секретам в контейнере: Приложения читают секрет из смонтированного пути к файлу.
    
    

В коде вашего приложения на Python (или аналогично для других языков)

with open('/run/secrets/db_password_secret', 'r') as f: db_password = f.read().strip()

with open('/run/secrets/sendgrid_api_key', 'r') as f: sendgrid_key = f.read().strip() ```

Docker Compose и секреты (для одного хоста или Swarm)

Docker Compose версии 3.1+ представил раздел secrets, который позволяет определять секреты и ссылаться на них в вашем docker-compose.yml. При работе в режиме Swarm Compose использует встроенные секреты Docker Swarm. При работе на одном хосте без режима Swarm Compose все равно поддерживает секреты, безопасно монтируя файлы с хоста в контейнер, хотя и без шифрования в покое, предоставляемого Swarm.

Использование secrets в docker-compose.yml

  1. Определите секреты: Вы можете определить секреты, либо ссылаясь на внешний файл, либо сделав его внешним секретом (предварительно созданным секретом Swarm).

    # docker-compose.yml
    version: '3.8'
    
    services:
      webapp:
        image: my_app_image:latest
        ports:
          - "80:8080"
        secrets:
          - db_password
          - sendgrid_api_key
    
    secrets:
      db_password:
        file: ./secrets/db_password.txt # Путь к файлу на хосте, содержащему пароль
      sendgrid_api_key:
        external: true                   # Ссылается на существующий секрет Docker Swarm с именем 'sendgrid_api_key'
    
  2. Создайте локальные файлы секретов (если используется file):

    
    

mkdir secrets echo "my_local_db_password" > ./secrets/db_password.txt ```

  1. Разверните с помощью Compose: docker compose up -d развернет ваши сервисы, сделав секреты доступными по пути /run/secrets/<secret_name> внутри контейнеров.

    # Внутри контейнера содержимое ./secrets/db_password.txt будет находиться по адресу:
    # /run/secrets/db_password
    

Выбор правильного инструмента: Конфигурация против секретов

Решение о том, использовать ли переменную окружения для конфигурации или специальное решение для управления секретами, сводится к одному основному вопросу:

Являются ли данные конфиденциальными?

  • Если Да (конфиденциальные данные): Используйте Docker Secrets (со Swarm) или аналогичную систему управления секретами (например, Kubernetes Secrets, HashiCorp Vault). Для однохостовых настроек Compose используйте раздел secrets для безопасного монтирования файлов.
  • Если Нет (неконфиденциальная конфигурация): Используйте переменные окружения (через ENV в Dockerfile, флаг -e, env_file или environment в Compose).
Особенность Переменные окружения (для конфигурации) Docker Secrets (для конфиденциальных данных)
Назначение Неконфиденциальная конфигурация приложения Конфиденциальные данные, такие как пароли и ключи API
Видимость Видны через docker inspect и часто при проверке процессов Монтируются как файлы; не отображаются как обычные переменные окружения
Безопасность Не подходит для конфиденциальных данных Более надежная обработка; секреты Swarm зашифрованы в покое, в то время как локальные файловые секреты Compose зависят от защиты файлов хоста
Доступ в приложении Чтение из os.environ или аналогичного Чтение из файла /run/secrets/<secret_name>
Управляется Среда выполнения Docker, Docker Compose Docker Swarm, Docker Compose или внешний менеджер секретов
Варианты использования Номера портов, флаги отладки, неконфиденциальные URL-адреса Пароли баз данных, токены API, частные ключи

Лучшие практики для обоих

Для конфигурации (переменные окружения):

  • Предоставляйте разумные значения по умолчанию в вашем Dockerfile с помощью ENV. Это делает ваши образы готовыми к запуску и четко документирует ожидаемые переменные.
  • Внешняя конфигурация где это возможно. Используйте файлы .env с docker compose или внешние сервисы конфигурации для более крупных развертываний.
  • Документируйте все параметры конфигурации и их ожидаемые значения, возможно, в README.md или документации приложения.
  • Избегайте жесткого кодирования значений, которые могут различаться в разных средах (разработка, тестирование, продакшен).

Для секретов (Docker Secrets и далее):

  • Никогда не фиксируйте секреты (например, файлы .env, содержащие секреты, db_password.txt) в системах контроля версий, таких как Git.
  • Регулярно меняйте секреты. Это минимизирует окно раскрытия в случае компрометации секрета.
  • Предоставляйте минимальные привилегии. Предоставляйте сервисам доступ только к тем секретам, которые им абсолютно необходимы.
  • Избегайте логирования значений секретов. Убедитесь, что ваше приложение и инфраструктурное логирование не выводят содержимое секретов.
  • Для крупномасштабных корпоративных развертываний рассмотрите специализированные решения для управления секретами, такие как HashiCorp Vault, AWS Secrets Manager или Azure Key Vault, которые предлагают более продвинутые функции, такие как аудит, динамическая генерация секретов и интеграция с управлением идентификацией и доступом (IAM).

Практическое правило для решения, что куда помещать

Прежде чем добавить значение в environment, спросите себя, что произойдет, если коллега вставит его в тикет поддержки. Если ответ «ничего серьезного», это, вероятно, конфигурация. Если ответ «нам придется сменить учетные данные», это секрет.

Хорошие переменные окружения:

APP_ENV=production
LOG_LEVEL=info
PUBLIC_BASE_URL=https://example.com
FEATURE_SIGNUP_ENABLED=false
REDIS_HOST=redis

Плохие переменные окружения:

DATABASE_PASSWORD=...
STRIPE_SECRET_KEY=...
JWT_SIGNING_KEY=...
AWS_SECRET_ACCESS_KEY=...
PRIVATE_SSH_KEY=...

Есть серые зоны. Имя хоста базы данных обычно является конфигурацией. Полный URL-адрес базы данных, включающий имя пользователя и пароль, является секретом. Публичный ключ аналитики может быть безопасным для браузерного приложения, в то время как частный токен API для того же поставщика — нет. Если сомневаетесь, считайте значение конфиденциальным, пока не докажете обратное.

Файлы .env Compose легко неправильно понять

Docker Compose использует .env двумя разными способами, которые люди часто путают.

Во-первых, Compose читает файл .env на уровне проекта для подстановки переменных внутри compose.yml:

services:
  web:
    image: "${APP_IMAGE}"
    ports:
      - "${HOST_PORT}:8080"

Во-вторых, env_file передает переменные в контейнер:

services:
  web:
    image: my-app
    env_file:
      - ./app.env

Эти файлы могут выглядеть одинаково, но служат разным целям. Первый помогает Compose отобразить конфигурацию. Второй становится средой выполнения внутри контейнера. Не предполагайте, что значение в проекте .env автоматически появляется внутри контейнера, если вы явно не передадите его.

Для локальной разработки полезен проверенный пример файла:

# .env.example
APP_ENV=development
LOG_LEVEL=debug
PUBLIC_BASE_URL=http://localhost:3000

Затем храните настоящий .env вне Git:

.env
*.env.local
secrets/

Пример файла документирует, что ожидает приложение, не раскрывая частные значения.

Чтение файловых секретов в приложении

Многие приложения уже ожидают секреты в переменных окружения. Переход к файловым секретам проще, если вы некоторое время поддерживаете оба шаблона.

Например, вспомогательная функция Node.js:

import fs from "node:fs";

function readSecret(name) {
  const filePath = process.env[`${name}_FILE`];
  if (filePath) {
    return fs.readFileSync(filePath, "utf8").trim();
  }
  return process.env[name];
}

const databasePassword = readSecret("DATABASE_PASSWORD");

Тогда ваш файл Compose может указывать на смонтированный файл секрета:

services:
  web:
    image: my-app
    environment:
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

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

Не помещайте секреты и в аргументы сборки

Переменные окружения — не единственная ловушка. Аргументы сборки также могут просочиться, если вы используете их для получения частных пакетов или клонирования репозиториев:

ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN

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

# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
  NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci

Соберите это так:

docker build \
  --secret id=npm_token,src=.npm-token \
  -t my-app .

Это сохраняет токен вне Dockerfile и предотвращает его встраивание в обычный слой. Вам все равно нужно защитить локальный файл .npm-token и хранилище секретов CI.

Kubernetes, облачные менеджеры секретов и Docker

Docker Secrets полезны в Swarm, а секреты Compose полезны для локальных или однохостовых настроек. В Kubernetes вы обычно будете использовать Kubernetes Secrets, внешний оператор секретов или интеграцию с облачным менеджером секретов. В AWS команды часто используют AWS Secrets Manager или Systems Manager Parameter Store. В Azure распространен Azure Key Vault. В Google Cloud ту же роль выполняет Secret Manager.

Принцип одинаков на всех платформах:

  • Храните конфиденциальные значения в системе, предназначенной для секретов.
  • Предоставьте идентификатору времени выполнения доступ только к тем секретам, которые ему нужны.
  • Монтируйте или внедряйте секреты во время выполнения.
  • Меняйте секреты без пересборки образа.
  • Храните секреты вне системы контроля версий, слоев образа, логов и панелей мониторинга.

Kubernetes Secrets по умолчанию закодированы, но не автоматически зашифрованы в каждой конфигурации кластера. Многие управляемые кластеры поддерживают шифрование в покое, но проверьте фактические настройки кластера, а не предполагайте. Для учетных данных высокого риска используйте облачный менеджер секретов или специальный инструмент с журналами аудита и поддержкой ротации.

Ротация является частью дизайна

Стратегия секретов, которая не может вращаться, незавершена. Задайте эти вопросы перед продакшеном:

  • Можем ли мы изменить пароль базы данных без пересборки образа?
  • Могут ли два действительных учетных данных перекрываться во время развертывания?
  • Перечитывает ли приложение секреты или ему нужен перезапуск?
  • Где старые учетные данные логируются, кэшируются или хранятся?
  • Кто получает уведомление при изменении секрета?

Для баз данных ротация часто означает создание второго учетного данных, развертывание приложения с новыми учетными данными, проверку трафика, а затем отзыв старых. Для ключей API это зависит от поставщика. Некоторые сервисы допускают несколько активных ключей; другие вынуждают к переключению. Проектируйте свой процесс развертывания с учетом наименее гибкой зависимости.

Очистка случайного раскрытия

Если секрет уже был зафиксирован в Git или встроен в образ, удаление строки недостаточно. Считайте его раскрытым.

Обычный ответ:

  1. Отозвать или сменить учетные данные.
  2. Удалить их из текущего кода или образа.
  3. Проверить логи CI, реестры образов, трекеры задач и чаты на наличие копий.
  4. Переписать историю Git только в том случае, если ваша организация готова к координации; ротация все равно требуется.
  5. Добавить сканирование или предварительные проверки для уменьшения повторяющихся ошибок.

Инструменты могут помочь, но они не заменяют привычки. Называйте файлы секретов четко, игнорируйте их в Git и избегайте массовой печати объектов конфигурации при запуске.

Рабочий шаблон

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

Чистый образ Docker одинаков во всех средах. Разработка, тестирование и продакшен меняют поведение во время выполнения. Конфигурация может передаваться как переменные окружения. Секреты должны поступать из хранилища секретов или смонтированного файла секретов с ограниченным доступом. Такое разделение сохраняет развертывания гибкими, не превращая каждую проверку контейнера, строку лога или слой образа в утечку учетных данных.