Перейти к основному содержимому

ADR: Выбор способа конфигурации приложения

  • Status: Accepted by Andrei Ganiushkin andrey@ganyushkin.ru
  • Date: 2026-07-11
  • Authors: Andrei Ganiushkin andrey@ganyushkin.ru
  • Related issues: -

Context

В рамках ADR стоит задача выбрать один, бескомпромиссно безопасный способ конфигурации приложения, который минимизирует человеческий фактор, случайные утечки данных, уязвимости файловой системы и утечки через механизмы инспекции процессов операционной системы, строго соответствуя концепции Zero-Trust.

Продукт должен иметь возможность разворачиваться в гетерогенных средах: на «голом железе» (Bare-metal), виртуальных машинах (VM), в Docker и Kubernetes (K8s) без перекомпиляции бинарного файла под конкретного заказчика.

Decision

Выбрать передачу конфигурации исключительно через стандартный ввод процесса (stdin) в формате YAML как единственный способ передачи рабочей конфигурации приложения.

Оператор, init-система, контейнерный runtime или оркестратор обязан передать YAML-документ в стандартный ввод программы в момент запуска. Приложение не должно читать рабочую конфигурацию из файлов, переменных окружения или аргументов командной строки.

Правила реализации решения на уровне кода Rust:

  1. Единый источник истины: При запуске рабочей команды приложение считывает полный YAML-документ только из stdin.
  2. Полный отказ от альтернатив: Любая поддержка чтения рабочей конфигурации из файлов (.yaml, .json, .toml, .env), переменных окружения и аргументов командной строки не допускается.
  3. CLI не является каналом конфигурации: Аргументы командной строки могут выбирать только режим работы приложения, например запуск сервиса или печать дефолтного шаблона конфигурации, но не должны содержать параметры бизнес-логики или секреты.
  4. Fail-Fast Валидация: На этапе запуска YAML должен быть полностью прочитан, десериализован и провалидирован. При синтаксической ошибке, отсутствии обязательных полей или логическом конфликте правил изоляции приложение мгновенно завершает работу с кодом 1.
  5. Отсутствие персистентного следа: Приложение не создает, не читает и не обновляет файлы рабочей конфигурации на диске.
  6. Кастомный panic-hook: Стандартный обработчик паники должен быть переопределен через panic::set_hook, чтобы при критических сбоях приложение не выводило сырой YAML, секреты или полную структуру конфигурации в логи.

Considered Alternatives

Alternative 1: Хранение конфигурации в файлах (YAML, TOML, JSON, .env)

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

  • Pros: Удобно описывать сложные, глубоко вложенные древовидные структуры данных и массивы.
  • Cons: Критически небезопасно.
    • Файлы конфигурации уязвимы к атакам класса Directory Traversal и Local File Inclusion (LFI) — если в логике изоляции возникнет брешь, злоумышленник сможет прочитать файл с диска.
    • Файлы .env и YAML часто случайно коммитятся в публичные или корпоративные Git-репозитории, а также автоматически включаются в незашифрованные системные бэкапы.
    • В Docker/K8s хранение настроек в файле внутри контейнера нарушает неизменяемость образов (Immutability) и усложняет динамическое управление секретами.
  • Reason for rejection: Способ признан фундаментально небезопасным для систем защиты информации. Он оставляет персистентный цифровой след чувствительных данных на жестком диске.

Alternative 2: Передача конфигурации через аргументы командной строки (CLI)

Все параметры передаются флагами при запуске бинарного файла (например, ./netgap-gateway --port 80).

  • Pros: Высокая скорость точечной настройки при ручном запуске.
  • Cons: Критически небезопасно.
    • Аргументы командной строки в Linux абсолютно открыты для инспекции процессов (Process Snooping). Любой непривилегированный пользователь или соседний сервис в системе может выполнить команду ps aux или cat /proc/<PID>/cmdline и увидеть все секреты в открытом виде.
    • Все переданные аргументы навсегда сохраняются на жестком диске в истории команд терминала (например, .bash_history или .zsh_history).
  • Reason for rejection: Полностью компрометирует конфиденциальность параметров безопасности на уровне ядра ОС.

Alternative 3: Передача конфигурации через переменные окружения (Environment Variables)

Все параметры и секреты передаются в окружение процесса, например через NETGAP_* переменные.

  • Pros: Хорошо поддерживается Docker, Kubernetes, systemd и внешними secret-management системами.
  • Cons: Небезопасно.
    • Переменные окружения являются частью состояния процесса, управляемого операционной системой, и могут быть раскрыты через механизмы инспекции окружения, дампы, диагностические инструменты или логи запуска.
    • В Linux окружение процесса экспонируется через /proc/<PID>/environ и может быть прочитано субъектом с достаточными правами доступа к процессу.
    • Значения переменных могут сохраняться в конфигурации init-систем, манифестах оркестраторов, истории деплоя и системных хранилищах.
    • Даже последующее удаление переменных из окружения процесса не гарантирует очистку области окружения, которую ядро Linux отдает через /proc/<PID>/environ; надежная перезапись этой области требует небезопасной низкоуровневой работы с памятью процесса и несовместима с политикой #![forbid(unsafe_code)].
  • Reason for rejection: Способ признан уязвимым, так как секреты попадают в хранилище и механизмы управления окружением операционной системы и инфраструктуры.

Consequences

Positive

  • Минимизация персистентного следа: Рабочая конфигурация не должна храниться в файлах приложения, переменных окружения или аргументах процесса.
  • Поддержка сложных структур: YAML естественно описывает вложенные структуры, списки правил, сетевые политики и параметры наблюдаемости без искусственного кодирования в плоские строки.
  • Единый формат для всех сред: Один и тот же бинарный файл без изменений работает на Bare-metal, VM, Docker и Kubernetes, если среда запуска умеет безопасно передать YAML в stdin.
  • Более безопасная диагностика: Кастомный panic::set_hook снижает риск утечки сырой конфигурации при аварийном завершении.

Negative

  • Среда запуска должна уметь безопасно сформировать и передать YAML-документ в stdin без записи на диск.
  • Нельзя использовать привычные механизмы конфигурации через ENV, .env, mounted config files или CLI-флаги с параметрами.
  • При ручном запуске оператор обязан избегать shell-конструкций, которые создают временные файлы или сохраняют секреты в истории команд.

Risks and Mitigations

  • Risk: Логирование сырого YAML или структуры конфигурации сторонними крэйтами/ПО при инициализации или падении приложения.
    • Mitigation: Использовать кастомный хук паники (panic::set_hook). Для структур конфигурации запрещать небезопасный автоматический вывод чувствительных полей и при необходимости реализовывать std::fmt::Debug вручную с редактированием секретов.
  • Risk: Небезопасная передача YAML оператором через файл или shell history.
    • Mitigation: Документировать безопасные способы запуска, при которых YAML поступает в stdin из доверенного secret-provider, pipe или supervisor-механизма без сохранения рабочей конфигурации на диск.
  • Risk: Частичное чтение или некорректная структура входного YAML.
    • Mitigation: Читать stdin полностью до старта сервиса, выполнять строгий YAML parsing и fail-fast валидацию до инициализации сетевых компонентов.

Acceptance Criteria

  • Рабочая команда считывает конфигурацию из stdin как YAML-документ.
  • Кодовая база приложения не содержит dotenv-подхода и не читает рабочую конфигурацию из файлов на диске.
  • Переменные окружения не используются как источник рабочей конфигурации приложения.
  • CLI-аргументы не принимают параметры бизнес-логики, сетевой политики или секреты; CLI может использоваться только для выбора команды, например запуска или печати дефолтного шаблона.
  • При пустом, частичном или невалидном YAML во stdin приложение завершается с кодом 1 без вывода секретов в stderr.
  • Кастомный panic::set_hook установлен до чтения и парсинга конфигурации.

Implementation Plan

  • Step 1: Оставить CLI только для выбора режима работы приложения, без передачи параметров конфигурации.
  • Step 2: Реализовать чтение полного YAML-документа из stdin для рабочей команды запуска.
  • Step 3: Реализовать десериализацию YAML в типизированную структуру конфигурации приложения.
  • Step 4: Добавить fail-fast валидацию структуры до инициализации наблюдаемости, сетевых слушателей и бизнес-логики.
  • Step 5: Реализовать команду печати дефолтного YAML-шаблона в stdout для операторов и автоматизации.
  • Step 6: Установить кастомный panic::set_hook до чтения конфигурации и исключить вывод сырой конфигурации в аварийных сообщениях.

Verification

  • Тест чтения из stdin: Передать валидный YAML в стандартный ввод рабочей команды и убедиться, что приложение успешно стартует.
  • Тест отказа от ENV: Запустить приложение с заполненными переменными NETGAP_*, но без YAML во stdin; убедиться, что переменные не используются как конфигурация.
  • Тест отказа от файлов: Убедиться с помощью утилит трассировки системных вызовов (например, strace в Linux), что приложение при старте не совершает системных вызовов open или openat к файлам рабочей конфигурации на диске.
  • Тест отказа от CLI-конфигурации: Попытаться передать параметры бизнес-логики через аргументы командной строки и убедиться, что такой интерфейс отсутствует или вызывает ошибку запуска.
  • Тест валидации: Передать пустой, частичный или синтаксически некорректный YAML в stdin. Убедиться, что приложение падает с кодом 1 без вывода содержимого секретов в stderr.
  • Тест паник-хука: Вызвать контролируемую панику после установки panic::set_hook и убедиться, что stderr/logs не содержат сырой YAML и секреты.

References

  • Документация по безопасности ядра Linux: управление доступом к файловой системе /proc.
  • Практики безопасного программирования на Rust.
  • Rust std::io: чтение данных из стандартного ввода процесса.