backlog: матрица поддержки файловых секретов по приложениям
- Проверено по исходникам закреплённых версий: outline, wakapi, gitea, gramps и authelia умеют брать секреты из файла, tududi и wanderer — нет. - Отмечено, что `secrets-env-to-file` переросла в эпик: разбивать по приложениям отдельным заходом.
This commit is contained in:
@@ -5,32 +5,87 @@
|
||||
miniflux сделан правильно — роль `secrets` кладёт каждую переменную vault в
|
||||
отдельный файл, каталог монтируется `:ro`, а compose ссылается на `*_FILE`
|
||||
(`files/miniflux/docker-compose.template.yml`). Остальные держат секреты прямыми
|
||||
значениями в `environment:`:
|
||||
|
||||
- **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`)
|
||||
значениями в `environment:`: их читает любой член группы `docker`, любой процесс
|
||||
с доступом к `/proc/<pid>/environ`, и они попадают в `docker inspect`.
|
||||
|
||||
tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и
|
||||
calibre тоже чистые.
|
||||
|
||||
Такие переменные читает любой член группы `docker`, любой процесс с доступом к
|
||||
`/proc/<pid>/environ`, и они попадают в `docker inspect`. Где приложение умеет
|
||||
`*_FILE` или конфиг-файл — постепенно перевести по образцу miniflux.
|
||||
## Матрица поддержки (проверено по исходникам, 2026-07-25)
|
||||
|
||||
Второй мотив, найденный позже: пока секрет стоит прямым значением в шаблоне,
|
||||
`ansible-playbook --diff` печатает его в терминал при каждом изменении файла.
|
||||
Вынос в отдельный файл чинит это структурно — дифф compose остаётся читаемым, а
|
||||
секретный файл рендерит роль `secrets` под `no_log`. Но покрывает только
|
||||
`environment:`; конфиги (authelia, bifrost, backups, remembos) и общий запрет на
|
||||
вывод — в [no-secrets-in-playbook-output](no-secrets-in-playbook-output.md), и
|
||||
она идёт первой: там дёшево и закрывает всё сразу.
|
||||
Проверялись **закреплённые в 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-secrets-in-playbook-output.md),
|
||||
и она идёт первой: `no_log` дёшев, закрывает все приложения разом и не зависит от
|
||||
этой матрицы. Для tududi и wanderer он вообще единственный ответ.
|
||||
|
||||
Матрица показала, что задача переросла в эпик: каждая строка — отдельное
|
||||
приложение со своим механизмом, и каждое мерджится независимо и само по себе
|
||||
даёт пользу. Договорились разбить по приложениям отдельным заходом (2026-07-25);
|
||||
до этого задачу в работу не брать целиком.
|
||||
|
||||
Из sec-ревью (fable, 2026-07-13). Связка с
|
||||
[service-users-docker-group](service-users-docker-group.md).
|
||||
|
||||
Связано: 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