backlog: матрица поддержки файловых секретов по приложениям
- Проверено по исходникам закреплённых версий: outline, wakapi, gitea, gramps и authelia умеют брать секреты из файла, tududi и wanderer — нет. - Отмечено, что `secrets-env-to-file` переросла в эпик: разбивать по приложениям отдельным заходом.
This commit is contained in:
@@ -59,8 +59,14 @@ dozzle, goaccess, rssbridge, homepage.
|
|||||||
независимо от того, поедут ли структурные задачи.
|
независимо от того, поедут ли структурные задачи.
|
||||||
2. Где файл смешанный (compose, конфиг authelia) — вернуться и вынести секреты в
|
2. Где файл смешанный (compose, конфиг authelia) — вернуться и вынести секреты в
|
||||||
отдельные файлы, сняв `no_log` с основного: он нужен ради диффа, который сейчас
|
отдельные файлы, сняв `no_log` с основного: он нужен ради диффа, который сейчас
|
||||||
и теряется. Для authelia отдельно проверить, что из девяти секретов умеет
|
и теряется. Что именно умеет каждое приложение — матрица в
|
||||||
file-based подстановку — конфиг на тысячу строк, дифф там ценный.
|
[secrets-env-to-file](secrets-env-to-file.md), сверенная по исходникам. Для
|
||||||
|
authelia (конфиг на тысячу строк, дифф там самый ценный) ответ такой: пять
|
||||||
|
полей закрываются `AUTHELIA_..._FILE`, оставшиеся два — только
|
||||||
|
template-фильтром, а четыре `client_secret` штатно хранятся хэшем и тогда
|
||||||
|
вообще перестают быть секретами. Для tududi и wanderer ответа нет: они не
|
||||||
|
умеют читать секреты из файлов, и `no_log` для них — не первый шаг, а
|
||||||
|
единственный.
|
||||||
3. Защита от дрейфа: проверка в lefthook или CI — если шаблон ссылается на
|
3. Защита от дрейфа: проверка в lefthook или CI — если шаблон ссылается на
|
||||||
переменную, которой нет в открытых `group_vars`/`vars`, задача, которая его
|
переменную, которой нет в открытых `group_vars`/`vars`, задача, которая его
|
||||||
рендерит, обязана иметь `no_log`. Иначе следующее приложение заведут по
|
рендерит, обязана иметь `no_log`. Иначе следующее приложение заведут по
|
||||||
|
|||||||
@@ -5,32 +5,87 @@
|
|||||||
miniflux сделан правильно — роль `secrets` кладёт каждую переменную vault в
|
miniflux сделан правильно — роль `secrets` кладёт каждую переменную vault в
|
||||||
отдельный файл, каталог монтируется `:ro`, а compose ссылается на `*_FILE`
|
отдельный файл, каталог монтируется `:ro`, а compose ссылается на `*_FILE`
|
||||||
(`files/miniflux/docker-compose.template.yml`). Остальные держат секреты прямыми
|
(`files/miniflux/docker-compose.template.yml`). Остальные держат секреты прямыми
|
||||||
значениями в `environment:`:
|
значениями в `environment:`: их читает любой член группы `docker`, любой процесс
|
||||||
|
с доступом к `/proc/<pid>/environ`, и они попадают в `docker inspect`.
|
||||||
- **outline** — пароль postgres, `SECRET_KEY`, `UTILS_SECRET`, секрет OIDC, SMTP
|
|
||||||
- **tududi** — session secret, секрет OIDC, ключ LLM, SMTP
|
|
||||||
- **wakapi** — salt паролей, секрет OIDC, SMTP
|
|
||||||
- **wanderer** — мастер-ключ meilisearch, ключ шифрования pocketbase
|
|
||||||
- **gramps** — `gramps_secret_key`, SMTP
|
|
||||||
- **gitea** — только SMTP (`postbox_pass`)
|
|
||||||
|
|
||||||
tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и
|
tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и
|
||||||
calibre тоже чистые.
|
calibre тоже чистые.
|
||||||
|
|
||||||
Такие переменные читает любой член группы `docker`, любой процесс с доступом к
|
## Матрица поддержки (проверено по исходникам, 2026-07-25)
|
||||||
`/proc/<pid>/environ`, и они попадают в `docker inspect`. Где приложение умеет
|
|
||||||
`*_FILE` или конфиг-файл — постепенно перевести по образцу miniflux.
|
|
||||||
|
|
||||||
Второй мотив, найденный позже: пока секрет стоит прямым значением в шаблоне,
|
Проверялись **закреплённые в compose версии**, не `main`.
|
||||||
`ansible-playbook --diff` печатает его в терминал при каждом изменении файла.
|
|
||||||
Вынос в отдельный файл чинит это структурно — дифф compose остаётся читаемым, а
|
| Приложение | Секрет | Файл | Механизм |
|
||||||
секретный файл рендерит роль `secrets` под `no_log`. Но покрывает только
|
|---|---|---|---|
|
||||||
`environment:`; конфиги (authelia, bifrost, backups, remembos) и общий запрет на
|
| **outline** `1.9.2` | `SECRET_KEY`, `UTILS_SECRET`, OIDC, SMTP | да | generic: `environment.ts` оборачивает `process.env` в Proxy, пустая переменная читается из `<NAME>_FILE`. С v1.7.0 |
|
||||||
вывод — в [no-secrets-in-playbook-output](no-secrets-in-playbook-output.md), и
|
| | пароль 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-secrets-in-playbook-output.md),
|
||||||
|
и она идёт первой: `no_log` дёшев, закрывает все приложения разом и не зависит от
|
||||||
|
этой матрицы. Для tududi и wanderer он вообще единственный ответ.
|
||||||
|
|
||||||
|
Матрица показала, что задача переросла в эпик: каждая строка — отдельное
|
||||||
|
приложение со своим механизмом, и каждое мерджится независимо и само по себе
|
||||||
|
даёт пользу. Договорились разбить по приложениям отдельным заходом (2026-07-25);
|
||||||
|
до этого задачу в работу не брать целиком.
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13). Связка с
|
Из sec-ревью (fable, 2026-07-13). Связка с
|
||||||
[service-users-docker-group](service-users-docker-group.md).
|
[service-users-docker-group](service-users-docker-group.md).
|
||||||
|
|
||||||
Связано: files/outline, files/wakapi, files/tududi, files/wanderer, files/gramps,
|
Связано: files/outline, files/wakapi, files/tududi, files/wanderer, files/gramps,
|
||||||
files/gitea, files/miniflux (образец), roles/secrets.
|
files/gitea, files/authelia, files/miniflux (образец), roles/secrets.
|
||||||
|
|||||||
Reference in New Issue
Block a user