diff --git a/docs/backlog/no-secrets-in-playbook-output.md b/docs/backlog/no-secrets-in-playbook-output.md index 8774c79..f2c3ed1 100644 --- a/docs/backlog/no-secrets-in-playbook-output.md +++ b/docs/backlog/no-secrets-in-playbook-output.md @@ -59,8 +59,14 @@ dozzle, goaccess, rssbridge, homepage. независимо от того, поедут ли структурные задачи. 2. Где файл смешанный (compose, конфиг authelia) — вернуться и вынести секреты в отдельные файлы, сняв `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 — если шаблон ссылается на переменную, которой нет в открытых `group_vars`/`vars`, задача, которая его рендерит, обязана иметь `no_log`. Иначе следующее приложение заведут по diff --git a/docs/backlog/secrets-env-to-file.md b/docs/backlog/secrets-env-to-file.md index dca02d1..ce7088b 100644 --- a/docs/backlog/secrets-env-to-file.md +++ b/docs/backlog/secrets-env-to-file.md @@ -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//environ`, и они попадают в `docker inspect`. tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и calibre тоже чистые. -Такие переменные читает любой член группы `docker`, любой процесс с доступом к -`/proc//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, пустая переменная читается из `_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/`, +то есть **тот же способ доставки**, что и `*_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.