Files
pet-project-server/docs/backlog/secrets-env-to-file.md
T
av f9a8c64377 backlog: матрица поддержки файловых секретов по приложениям
- Проверено по исходникам закреплённых версий: outline, wakapi, gitea,
  gramps и authelia умеют брать секреты из файла, tududi и wanderer — нет.
- Отмечено, что `secrets-env-to-file` переросла в эпик: разбивать по
  приложениям отдельным заходом.
2026-07-25 12:20:20 +03:00

8.3 KiB
Raw Blame History

Секреты приложений через *_FILE, а не inline environment:

Приоритет: средний

miniflux сделан правильно — роль secrets кладёт каждую переменную vault в отдельный файл, каталог монтируется :ro, а compose ссылается на *_FILE (files/miniflux/docker-compose.template.yml). Остальные держат секреты прямыми значениями в environment:: их читает любой член группы docker, любой процесс с доступом к /proc/<pid>/environ, и они попадают в docker inspect.

tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и calibre тоже чистые.

Матрица поддержки (проверено по исходникам, 2026-07-25)

Проверялись закреплённые в compose версии, не main.

Приложение Секрет Файл Механизм
outline 1.9.2 SECRET_KEY, UTILS_SECRET, OIDC, SMTP да generic: environment.ts оборачивает process.env в Proxy, пустая переменная читается из <NAME>_FILE. С v1.7.0
пароль postgres да, с оговоркой DATABASE_URL_FILE выносит URL целиком; чтобы вынести только пароль — раздельные DATABASE_HOST/PORT/NAME/USER + DATABASE_PASSWORD_FILE (взаимоисключимы с DATABASE_URL)
пароль сайдкара postgres да штатный POSTGRES_PASSWORD_FILE официального образа
wakapi 2.17.5 salt, SMTP, OIDC да два пути: generic _FILE в loadSecretFiles() (с 2.17.2, в README список устарел) либо config.yml в /app/config.yml
gitea 1.27.0 SMTP да, частично GITEA__mailer__PASSWD__FILE, работает для любого ключа app.ini, с 1.20
gramps 26.7.0 GRAMPSWEB_SECRET_KEY да entrypoint читает /app/secret/secret, если переменная не задана. Путь уже смонтирован в нашем compose
SMTP нет обходной путь есть, но недокументированный: config.cfg исполняется Flask как Python, значит EMAIL_HOST_PASSWORD = open(...).read() сработает
authelia 4.39.20 jwt-секрет, session, ключ хранилища, hmac OIDC, SMTP да AUTHELIA_..._FILE
jwks-ключ, client_secret × 4 нет через _FILE IsSecretKey() отвергает ключи с []. Покрывается template-фильтром: X_AUTHELIA_CONFIG_FILTERS=template + {{ secret "путь" }} (с 4.38)
tududi 1.2.4 все четыре нет всё через process.env; ни _FILE, ни конфига, ни CLI
wanderer 0.18.3 мастер-ключ meilisearch нет только CLI/env/инлайн в config.toml; апстрим отклонил запрос в 2023
ключ шифрования pocketbase нет собственная переменная wanderer, не штатный --encryptionEnv pocketbase; ENTRYPOINT в exec-форме, подстановку не сделать

Что из этого следует

Самое дешёвое — gramps: каталог /app/secret уже смонтирован, достаточно записать туда значение и убрать переменную из environment:. Оба сервиса (gramps_app и gramps_celery) читают тот же файл через общий entrypoint.

Authelia выгоднее переводить не на _FILE, а целиком на template-фильтр: _FILE покрывает пять полей из семи, фильтр — все разом, и смешивать их в одном поле нельзя (errFmtSecretAlreadyDefined). Отдельная находка: четыре client_secret штатно хранятся хэшем $pbkdf2-sha512$... (у нас уже есть inv authelia-gen-secret-and-hash), плейнтекст с 4.38 вызывает warning валидатора. То есть эти поля можно вообще перестать считать секретами.

Gitea даёт частичный выигрыш: секрет уходит из environment:, docker inspect и окружения процесса, но environment-to-ini пишет его открытым текстом в app.ini на диске — открытая проблема апстрима (#35316).

tududi и wanderer не умеют ничего. Для них остаётся либо принять env, либо перенести переменные в файл на диске: у tududi backend/app.js зовёт dotenv при рабочей директории /app/backend, поэтому смонтированный туда .env уберёт секреты хотя бы из docker inspect — в отличие от env_file:, где они попадают в Config.Env. У wanderer доступен только env_file:, то есть косметика.

Попутная находка в закомментированном OIDC-блоке wakapi (ловушка на будущее, не живой баг): имена WAKAPI_OIDC_PROVIDER_* устарели, в 2.17.5 разбирается WAKAPI_OIDC_PROVIDERS_(\d+)_(...); кроме того renameEnvVars() режет строку по первому =, так что секрет с base64-паддингом через env поедет битым. Оба довода — за config.yml.

Docker secrets — разобрано, покрытия не расширяет

secrets: в compose вне swarm — это bind-mount файла в /run/secrets/<name>, то есть тот же способ доставки, что и *_FILE. Приложение, умеющее только переменную окружения, из /run/secrets не прочитает — ограничение в приложении, а не в доставке. На таблицу выше не влияет.

Что даёт: секрет объявляется на сервис, а не монтируется каталогом. У miniflux сейчас {{ secrets_dir }}:/secrets:ro висит на обоих сервисах, и postgres видит все шесть файлов, включая секреты OIDC, хотя нужен ему один пароль. Улучшение поверх образца, а не замена ему. uid/gid/mode вне swarm игнорируются — права остаются с хост-файла, их ставит роль secrets.

Границы задачи

Она про то, где секрет лежит в рантайме (env против файла). Про то, чтобы он не печатался в терминал при деплое, — no-secrets-in-playbook-output, и она идёт первой: no_log дёшев, закрывает все приложения разом и не зависит от этой матрицы. Для tududi и wanderer он вообще единственный ответ.

Матрица показала, что задача переросла в эпик: каждая строка — отдельное приложение со своим механизмом, и каждое мерджится независимо и само по себе даёт пользу. Договорились разбить по приложениям отдельным заходом (2026-07-25); до этого задачу в работу не брать целиком.

Из sec-ревью (fable, 2026-07-13). Связка с service-users-docker-group.

Связано: files/outline, files/wakapi, files/tududi, files/wanderer, files/gramps, files/gitea, files/authelia, files/miniflux (образец), roles/secrets.