- Шесть задач `secrets-file-*` вместо одной: outline, wakapi, authelia, gramps (средний), gitea, tududi (низкий, выигрыш частичный). Wanderer отпал — файловых секретов не умеет ни meilisearch, ни pocketbase. - Матрица механизмов со ссылками на код уехала в docs/drafts/secrets-file-support.md, родитель — на кладбище.
15 KiB
Файловые секреты: что умеет каждое приложение
Дата: 2026-07-25. Статус: справка по итогам проверки исходников (не план работ).
Проверялись закреплённые в наших compose-файлах версии, не main. Источник —
код парсинга конфига в репозиториях приложений; доки использовались как
подтверждение, а не как основание. Из этой справки выросли задачи
secrets-file-* в беклоге, по одной на приложение.
Зачем
Секреты стоят прямыми значениями в environment: docker-compose. Такие
переменные читает любой член группы docker, любой процесс с доступом к
/proc/<pid>/environ, и они попадают в docker inspect. Цель — там, где
приложение умеет, брать секрет из файла.
Образец — miniflux: роль secrets кладёт каждую переменную vault в отдельный
файл, каталог монтируется :ro, compose ссылается на *_FILE
(files/miniflux/docker-compose.template.yml).
Отдельная и более важная цель — чтобы секреты не печатались в терминал при
деплое — этой справкой не решается: там работает no_log, см. задачу
no-secrets-in-playbook-output. Для tududi и wanderer no_log — единственный
доступный ответ.
Матрица
| Приложение | Секрет | Файл | Механизм |
|---|---|---|---|
outline 1.9.2 |
SECRET_KEY, UTILS_SECRET, OIDC, SMTP |
да | generic <NAME>_FILE |
| пароль postgres | да, с оговоркой | только вместе с переходом на раздельные DATABASE_* |
|
| пароль сайдкара postgres | да | штатный POSTGRES_PASSWORD_FILE |
|
wakapi 2.17.5 |
salt, SMTP, OIDC | да | generic _FILE либо config.yml |
gitea 1.27.0 |
SMTP | да, частично | GITEA__mailer__PASSWD__FILE, но значение оседает в app.ini |
gramps 26.7.0 |
GRAMPSWEB_SECRET_KEY |
да | файл /app/secret/secret, путь уже смонтирован |
| SMTP | нет | только недокументированный трюк с config.cfg |
|
authelia 4.39.20 |
jwt, session, ключ хранилища, hmac OIDC, SMTP | да | AUTHELIA_..._FILE |
jwks-ключ, client_secret × 4 |
нет через _FILE |
только template-фильтр; секреты клиентов лучше хэшировать | |
tududi 1.2.4 |
все четыре | нет | обходной путь — смонтированный .env |
wanderer 0.18.3 |
ключ meilisearch, ключ pocketbase | нет | ничего |
Подробности по приложениям
outline 1.9.2
server/utils/environment.ts оборачивает process.env в Proxy: если переменная
пуста, читается <NAME>_FILE как путь к файлу, содержимое обрезается по
пробелам. Весь server/env.ts и плагины читают именно через него, поэтому
механизм покрывает любую переменную, включая OIDC-плагин.
- Появилось в v1.7.0 (PR #11906, merged 2026-03-30), в v1.9.0 переписано на ленивый Proxy (PR #12889). Проверено бисекцией: в v1.6.1 механизма нет, в v1.7.0 есть.
- Конвенция описана в .env.sample строки 4–18, включая правило приоритета: заданы обе — побеждает прямая переменная.
- Пароль БД:
DATABASE_URL_FILEвыносит весь URL. Чтобы вынести только пароль, нужны раздельныеDATABASE_HOST/PORT/NAME/USER+DATABASE_PASSWORD, они взаимоисключимы сDATABASE_URLчерез@CannotUseWith(env.ts L95–L143). - Сайдкар
postgres:16.3— штатныйPOSTGRES_PASSWORD_FILE.
wakapi 2.17.5
Два независимых пути.
loadSecretFiles()(config.go L829) — generic: проходит по всему окружению, для любой переменной с суффиксом_FILEчитает файл,TrimSpace, кладёт в базовое имя, файловую снимает. Заданы обе — процесс падает с «both environment variables are set».- Порядок в
Load():loadSecretFiles()→renameEnvVars()→configor.Load, поэтому_FILEработает и для OIDC-переменных в configor-формате. - История: PR #679 (релиз 2.12.1)
сделал это в
entrypoint.shдля пяти переменных; коммитa45732b2(релиз 2.17.2, переход на distroless) перенёс логику в Go и сделал универсальной. README (L92) перечисляет только три переменные — список устарел относительно кода. config.ymlпокрывает все три поля:security.password_salt,security.oidc[],mail.smtp.password. Образ кладётconfig.default.ymlв/app/config.yml, туда же монтируется свой. Env перекрывает значения из yaml.
Две ловушки в нашем закомментированном OIDC-блоке (не живой баг, но при включении выстрелит):
- Имена
WAKAPI_OIDC_PROVIDER_CLIENT_IDустарели — в 2.17.5 разбираетсяWAKAPI_OIDC_PROVIDERS_(\d+)_([A-Z_]+), старые молча игнорируются. renameEnvVars()разбирает строку какstrings.Split(e, "=")и берётparts[1]— значение обрезается по первому=, то есть секрет с base64-паддингом через env поедет битым.
Оба довода — за config.yml для OIDC-секрета.
gitea 1.27.0
GITEA__mailer__PASSWD__FILE=/path — механизм универсальный, работает для любого
ключа app.ini, хвостовой перевод строки обрезается
(config_env.go L100–141,
PR #24832, с 1.20).
Оговорка, которая режет пользу: environment-to-ini записывает содержимое
файла в app.ini открытым текстом, то есть секрет всё равно оседает на
хостовом диске в <data_dir>/gitea/conf/app.ini. Открытая проблема апстрима
#35316 (ранее #25316), PR нет.
Выигрыш реальный, но частичный: уходит из environment:, docker inspect и
окружения процесса — не с диска.
gramps 26.7.0 (gramps-web-api v3.18.0)
Если GRAMPSWEB_SECRET_KEY не задана, entrypoint образа читает ключ из файла
/app/secret/secret (и генерирует, если файла нет) —
docker-entrypoint.sh.
Этот путь у нас уже смонтирован как gramps_secret. Entrypoint общий у
gramps_app и gramps_celery через YAML merge key, оба читают тот же файл.
SMTP-пароль файлового источника не имеет: конфиг читается через
app.config.from_prefixed_env(prefix="GRAMPSWEB"), никакой обработки _FILE
(app.py L99–121).
Обходной путь: Dockerfile задаёт GRAMPS_API_CONFIG=/app/config/config.cfg, а
from_envvar() во Flask — это from_pyfile(), то есть config.cfg исполняется
как Python, и EMAIL_HOST_PASSWORD = open("/run/secrets/smtp").read().strip()
сработает. Механизм рабочий, но держится на детали реализации Flask и нигде не
обещан.
authelia 4.39.20
IsSecretKey() возвращает false для любого ключа, содержащего [] (элемент
списка), и требует, чтобы ключ оканчивался на key/secret/password/token/
certificate_chain
(helpers.go).
Отсюда деление:
- Умеют
_FILE(5 полей):identity_validation.reset_password.jwt_secret,session.secret,storage.encryption_key,identity_providers.oidc.hmac_secret,notifier.smtp.password. - Не умеют (2 позиции):
identity_providers.oidc.jwks[].keyиclient_secretчетырёх клиентов — оба содержат[].
Для них есть template-фильтр
(X_AUTHELIA_CONFIG_FILTERS=template, с 4.38): {{ secret "/path" | nindent 10 }}.
Механизмы несовместимы в одном поле — если задано и _FILE, и значение в
конфиге, Authelia падает с errFmtSecretAlreadyDefined. Поэтому проще выбрать
один: фильтр покрывает все семь позиций, _FILE — только пять.
Отдельно про client_secret: штатная форма хранения — PHC-дайджест
$pbkdf2-sha512$310000$..., плейнтекст с 4.38 вызывает warning валидатора
(errFmtOIDCClientInvalidSecretPlainText). Плейнтекст обязателен только при
client_secret_jwt или симметричном шифровании JWT — у наших четырёх клиентов
(miniflux, wakapi, tududi — дефолт; outline — access_token_signed_response_alg: none) ничего такого нет. То есть эти поля можно перестать считать секретами:
в конфиге хэш, сам секрет живёт в vault и в конфиге клиентского приложения.
Генератор уже есть — inv authelia-gen-secret-and-hash.
tududi 1.2.4 — не умеет
Все четыре секрета читаются напрямую из process.env
(config.js L94, L40,
providerConfig.js L82,
service.js L20).
Греп по репозиторию на теге даёт только DB_FILE и FILE_UPLOAD_LIMIT_MB; ни
readFileSync для секретов, ни конфиг-файла, ни CLI-опции нет. Собственная дока
по OIDC советует «consider Docker secrets» — это совет про хранение, читать файлы
приложение не умеет.
Обходной путь: backend/app.js вызывает require('dotenv').config(), рабочая
директория контейнера — /app/backend, значит смонтированный туда .env
подхватится. Это не per-secret файлы, но значения не попадают в Config.Env —
в отличие от env_file:, где попадают. dotenv не перезаписывает уже
установленные переменные окружения.
wanderer 0.18.3 — не умеет ничего
MEILI_MASTER_KEY(meilisearch v1.20.0): только CLI--master-key, env или инлайн вconfig.toml;_FILEнет (option.rs). Апстрим отклонил запрос — discussion #201, 2023. Клиентская сторона (wanderer_db,wanderer_web) тоже читаетos.Getenv.POCKETBASE_ENCRYPTION_KEY— это не штатный механизм pocketbase (--encryptionEnv), а собственная переменная wanderer, читаетсяos.Getenvв четырёх местах (db/main.go). ENTRYPOINT в exec-форме, подстановку через шелл не сделать. Найденный в поискеPOCKETBASE_ENCRYPTION_KEY_FILE— фича стороннего образаadrianmusante/pocketbase, кflomp/wanderer-dbотношения не имеет.
Доступен только env_file:, где значения всё равно попадают в Config.Env, то
есть косметика. Задачи по wanderer не заведено.
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.