backlog: secrets-env-to-file разложена по приложениям
- Шесть задач `secrets-file-*` вместо одной: outline, wakapi, authelia, gramps (средний), gitea, tududi (низкий, выигрыш частичный). Wanderer отпал — файловых секретов не умеет ни meilisearch, ни pocketbase. - Матрица механизмов со ссылками на код уехала в docs/drafts/secrets-file-support.md, родитель — на кладбище.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Кладбище беклога
|
||||
- 2026-07-25 `secrets-env-to-file` — Секреты приложений через `*_FILE`, а не inline `environment:`. Причина: разложена по приложениям на secrets-file-outline/-wakapi/-authelia/-gramps/-gitea/-tududi; матрица поддержки уехала в docs/drafts/secrets-file-support.md, wanderer отпал — файловых секретов не умеет. Был приоритет: средний.
|
||||
@@ -25,7 +25,6 @@ _(sec-ревью)_ — три независимых security-ревью на м
|
||||
|
||||
## Средний
|
||||
|
||||
- [Секреты приложений через `*_FILE`, а не inline `environment:`](secrets-env-to-file.md) — outline/wakapi/tududi/gitea/gramps/tuwunel держат секреты в env; видны в `docker inspect` и `/proc` _(sec-ревью)_
|
||||
- [Захардить мониторинг-стек (netdata / dozzle / goaccess)](harden-monitoring-stack.md) — netdata/dozzle: лишние привилегии + docker.sock + доверие Remote-User в общей сети; socket-proxy + monitoring_network _(sec-ревью)_
|
||||
- [Ограничить эндпоинт `/metrics` Miniflux](miniflux-metrics-restrict.md) — `METRICS_ALLOWED_NETWORKS=0.0.0.0/0` + прокси без ограничения пути отдаёт метрики публично _(sec-ревью)_
|
||||
- [Закрыть calibre-web forward-auth и проверить дефолтные креды](calibre-forward-auth.md) — домен без forward_auth, защита на встроенной авторизации с дефолтом `admin/admin123` _(sec-ревью)_
|
||||
@@ -39,6 +38,10 @@ _(sec-ревью)_ — три независимых security-ревью на м
|
||||
- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_
|
||||
- [Локальная сборка образов вместо Yandex Container Registry](local-image-build.md) — homepage/transcriber/remembos тянут образы из cr.yandex, ради `docker login` в vault лежит OAuth-токен всего аккаунта; роль `app_image` уже обкатана в umbar
|
||||
- [Вывести из оборота секреты с широкими правами](narrow-secret-scopes.md) — один `yc_oauth_token` открывает весь аккаунт Yandex Cloud; после инцидента 24 июля цена утечки одной переменной должна быть ограничена одним сервисом
|
||||
- [outline: секреты из файлов](secrets-file-outline.md) — умеет generic `<NAME>_FILE` для всех пяти секретов; единственное приложение, где выносится вообще всё, включая пароль БД
|
||||
- [wakapi: секреты из файлов](secrets-file-wakapi.md) — salt, SMTP и OIDC выносятся либо через generic `_FILE`, либо через `config.yml`; для OIDC второй путь надёжнее из-за бага с `=` в значении
|
||||
- [authelia: секреты из файлов](secrets-file-authelia.md) — пять полей закрывает `_FILE`, jwks-ключ и client secret — только template-фильтр; заодно четыре client secret можно перевести в хэш и перестать считать секретами
|
||||
- [gramps: секретный ключ из файла](secrets-file-gramps.md) — entrypoint читает `/app/secret/secret`, если переменная не задана, и этот путь уже смонтирован — самая дешёвая правка из всей серии
|
||||
|
||||
## Низкий
|
||||
|
||||
@@ -50,3 +53,5 @@ _(sec-ревью)_ — три независимых security-ревью на м
|
||||
- [Фоновая зачистка стиля и конфигурации Ansible](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_
|
||||
- [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_
|
||||
- [[idea] Где хранить пароль ansible-vault](vault-password-storage.md) — пароль открытым текстом рядом с репо, `ansible.cfg` подставляет его в любую ansible-команду; вариант — доставать из KeePassXC
|
||||
- [gitea: пароль SMTP из файла](secrets-file-gitea.md) — умеет `GITEA__mailer__PASSWD__FILE`, но пишет значение открытым текстом в `app.ini` — выигрыш только против `docker inspect`
|
||||
- [tududi: секреты в смонтированный `.env`](secrets-file-tududi.md) — файловых секретов не умеет вовсе; единственный путь — смонтировать `.env` под `dotenv`, чтобы значения не попадали в `Config.Env`
|
||||
|
||||
@@ -39,4 +39,4 @@ transcriber и remembos. Радиус прав несопоставим с за
|
||||
Связано: group_vars/all/secrets.yml, playbook-homepage.yml,
|
||||
playbook-transcriber.yml, playbook-remembos.yml,
|
||||
[vault-password-storage](vault-password-storage.md),
|
||||
[secrets-env-to-file](secrets-env-to-file.md).
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
|
||||
@@ -28,7 +28,7 @@ miniflux деплоится молча, а остальные — нет.
|
||||
- **apprise** — `server.template.cfg`: токен бота Telegram, SMTP
|
||||
- **remembos** — `config.template.toml`: токены memos и Telegram
|
||||
- **outline, tududi, wakapi, wanderer, gramps, gitea** — секреты в
|
||||
`environment:` docker-compose, см. [secrets-env-to-file](secrets-env-to-file.md)
|
||||
`environment:` docker-compose, см. серию задач `secrets-file-*`
|
||||
|
||||
Чисто: miniflux (образец), memos, calibre, tuwunel, dashboard, caddy, netdata,
|
||||
dozzle, goaccess, rssbridge, homepage.
|
||||
@@ -48,8 +48,8 @@ dozzle, goaccess, rssbridge, homepage.
|
||||
для файлов, состоящих из одних секретов, слабее.
|
||||
- **Структурный вынос** — секрет живёт в отдельном файле, который рендерит роль
|
||||
`secrets` под `no_log`, а основной файл ссылается на путь. Тогда дифф основного
|
||||
файла остаётся полезным. Это [secrets-env-to-file](secrets-env-to-file.md), но
|
||||
она про `environment:` и не покрывает конфиги.
|
||||
файла остаётся полезным. Это серия `secrets-file-*`, но она про `environment:`
|
||||
и не покрывает конфиги.
|
||||
- **Убрать `--diff` из `inv pl`** или выставить глобальный `ANSIBLE_NO_LOG` —
|
||||
режет обзор на всех задачах разом ради десятка проблемных. Не годится.
|
||||
|
||||
@@ -60,7 +60,8 @@ dozzle, goaccess, rssbridge, homepage.
|
||||
2. Где файл смешанный (compose, конфиг authelia) — вернуться и вынести секреты в
|
||||
отдельные файлы, сняв `no_log` с основного: он нужен ради диффа, который сейчас
|
||||
и теряется. Что именно умеет каждое приложение — матрица в
|
||||
[secrets-env-to-file](secrets-env-to-file.md), сверенная по исходникам. Для
|
||||
[справке](../drafts/secrets-file-support.md), сверенная по исходникам, и
|
||||
задачи `secrets-file-*` по одной на приложение. Для
|
||||
authelia (конфиг на тысячу строк, дифф там самый ценный) ответ такой: пять
|
||||
полей закрываются `AUTHELIA_..._FILE`, оставшиеся два — только
|
||||
template-фильтром, а четыре `client_secret` штатно хранятся хэшем и тогда
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
# Секреты приложений через `*_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-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/authelia, files/miniflux (образец), roles/secrets.
|
||||
@@ -0,0 +1,32 @@
|
||||
# authelia: секреты из файлов
|
||||
|
||||
**Приоритет:** Средний
|
||||
|
||||
В `configuration.template.yml` прямыми значениями стоят семь позиций:
|
||||
jwt-секрет валидации, секрет сессий, ключ шифрования хранилища, hmac-секрет OIDC,
|
||||
приватный jwks-ключ, четыре `client_secret` и пароль SMTP. Файл на тысячу строк —
|
||||
дифф по нему самый ценный в репозитории, и именно его сейчас приходится глушить
|
||||
целиком.
|
||||
|
||||
Пять полей закрываются переменными `AUTHELIA_..._FILE`. Оставшиеся два —
|
||||
`jwks[].key` и `client_secret` клиентов — через `_FILE` невозможны в принципе:
|
||||
валидатор отвергает ключи, содержащие `[]`. Их покрывает template-фильтр
|
||||
(`X_AUTHELIA_CONFIG_FILTERS=template` + `{{ secret "путь" }}`). Механизмы
|
||||
несовместимы в одном поле, поэтому проще выбрать фильтр — он берёт все семь
|
||||
позиций разом, `_FILE` только пять.
|
||||
|
||||
Отдельная и более дешёвая половина задачи: четыре `client_secret` штатно хранятся
|
||||
**хэшем** `$pbkdf2-sha512$...` — плейнтекст с 4.38 вызывает warning валидатора и
|
||||
нужен только при `client_secret_jwt`, чего у наших клиентов нет. После перевода
|
||||
эти поля перестают быть секретами вообще. Генератор уже есть —
|
||||
`inv authelia-gen-secret-and-hash`, проверка — `inv authelia-validate-config`.
|
||||
|
||||
Готово, когда в отрендеренном конфиге не осталось секретов открытым текстом и
|
||||
`no_log` с задачи снят — дифф конфига снова читаем.
|
||||
|
||||
Разбор по каждому полю, версии и ссылки на код:
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/authelia, tasks.py (`authelia-*`), roles/secrets,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -0,0 +1,24 @@
|
||||
# gitea: пароль SMTP из файла
|
||||
|
||||
**Приоритет:** Низкий
|
||||
|
||||
Единственный секрет gitea в compose — пароль SMTP. Механизм есть:
|
||||
`GITEA__mailer__PASSWD__FILE` работает для любого ключа `app.ini` начиная с 1.20,
|
||||
у нас 1.27.0.
|
||||
|
||||
Приоритет низкий из-за оговорки, которая съедает половину пользы:
|
||||
`environment-to-ini` **записывает содержимое файла в `app.ini` открытым текстом**,
|
||||
то есть пароль всё равно оседает на диске в `<data_dir>/gitea/conf/app.ini`. Это
|
||||
открытая проблема апстрима (#35316), исправления нет. Выигрыш реальный, но
|
||||
частичный — секрет уходит из `environment:`, `docker inspect` и окружения
|
||||
процесса, а с диска не уходит.
|
||||
|
||||
Готово, когда `docker inspect` контейнера gitea не показывает пароль SMTP. Что он
|
||||
остался в `app.ini` — известное ограничение, а не незакрытая часть задачи.
|
||||
|
||||
Механизм и ссылки на код:
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/gitea, roles/secrets,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -0,0 +1,26 @@
|
||||
# gramps: секретный ключ из файла
|
||||
|
||||
**Приоритет:** Средний
|
||||
|
||||
`GRAMPSWEB_SECRET_KEY` стоит прямым значением в `environment:`. Если переменную
|
||||
не задавать, entrypoint образа читает ключ из файла `/app/secret/secret` — и этот
|
||||
путь **уже смонтирован** в нашем compose как `gramps_secret`. То есть правка
|
||||
сводится к «записать значение в файл и убрать переменную»: самая дешёвая из всей
|
||||
серии. Entrypoint общий у `gramps_app` и `gramps_celery`, оба читают тот же файл,
|
||||
расхождения не будет.
|
||||
|
||||
Пароль SMTP так не выносится: конфиг читается через `from_prefixed_env`, никакой
|
||||
обработки `_FILE` нет. Обходной путь существует (Flask исполняет `config.cfg` как
|
||||
Python, значит значение можно прочитать из файла прямо в нём), но он держится на
|
||||
детали реализации и нигде не обещан — брать только если решим, что оно того
|
||||
стоит. Пока пароль SMTP остаётся в env и закрывается `no_log`.
|
||||
|
||||
Готово, когда `docker inspect` контейнеров gramps не показывает секретный ключ
|
||||
приложения.
|
||||
|
||||
Механизм и ссылки на код:
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/gramps, roles/secrets,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -0,0 +1,24 @@
|
||||
# outline: секреты из файлов
|
||||
|
||||
**Приоритет:** Средний
|
||||
|
||||
Сейчас в `environment:` стоят прямыми значениями пароль postgres (внутри
|
||||
`DATABASE_URL`), `SECRET_KEY`, `UTILS_SECRET`, секрет OIDC и пароль SMTP. Outline
|
||||
`1.9.2` умеет generic-механизм: пустая переменная читается из `<NAME>_FILE`,
|
||||
поэтому все пять закрываются по образцу miniflux — роль `secrets` + монтирование
|
||||
каталога `:ro`.
|
||||
|
||||
Отдельный шаг внутри задачи — пароль БД: `DATABASE_URL_FILE` выносит **весь** URL
|
||||
одним куском, и чтобы вынести именно пароль, нужен переход на раздельные
|
||||
`DATABASE_HOST/PORT/NAME/USER` (они взаимоисключимы с `DATABASE_URL`). Заодно
|
||||
сайдкар postgres переводится на штатный `POSTGRES_PASSWORD_FILE`.
|
||||
|
||||
Готово, когда `docker inspect` контейнеров outline и его postgres не показывает
|
||||
ни одного секрета, а в compose-шаблоне не осталось jinja-переменных с секретами —
|
||||
и, как следствие, дифф этого шаблона снова можно смотреть без `no_log`.
|
||||
|
||||
Механизм и ссылки на код: [справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/outline, files/miniflux (образец), roles/secrets,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -0,0 +1,27 @@
|
||||
# tududi: секреты в смонтированный `.env`
|
||||
|
||||
**Приоритет:** Низкий
|
||||
|
||||
Tududi `1.2.4` не умеет файловых секретов вообще: session secret, секрет OIDC,
|
||||
ключ LLM и пароль SMTP читаются напрямую из `process.env`, конфиг-файла и
|
||||
CLI-опций нет. Проверено грепом по репозиторию на теге, не по докам.
|
||||
|
||||
Единственный доступный путь — не `env_file:` (при нём значения всё равно попадают
|
||||
в `Config.Env`), а **смонтированный файл**: `backend/app.js` вызывает `dotenv`, а
|
||||
рабочая директория контейнера — `/app/backend`, значит `.env` можно положить туда
|
||||
томом. Тогда секреты не видны в `docker inspect`, но остаются в окружении
|
||||
процесса и в `/proc/<pid>/environ`.
|
||||
|
||||
Приоритет низкий именно поэтому: механизм нештатный (держится на том, что
|
||||
`dotenv` остаётся в prod-зависимостях образа), а закрывает только один вектор из
|
||||
трёх. Если апстрим когда-нибудь добавит `_FILE` — задача переписывается на него.
|
||||
|
||||
Готово, когда `docker inspect` контейнера tududi не показывает ни одного из
|
||||
четырёх секретов.
|
||||
|
||||
Разбор и ссылки на код:
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/tududi,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -0,0 +1,28 @@
|
||||
# wakapi: секреты из файлов
|
||||
|
||||
**Приоритет:** Средний
|
||||
|
||||
Сейчас в `environment:` прямыми значениями стоят salt паролей и пароль SMTP,
|
||||
а в закомментированном блоке — секрет OIDC. Wakapi `2.17.5` даёт два пути, и
|
||||
выбрать надо один:
|
||||
|
||||
- **generic `_FILE`** — `loadSecretFiles()` разворачивает любую переменную с
|
||||
таким суффиксом. Ближе к образцу miniflux, меньше движений.
|
||||
- **`config.yml`**, смонтированный в `/app/config.yml` — покрывает те же три
|
||||
поля декларативно.
|
||||
|
||||
Для OIDC-секрета второй путь надёжнее: `renameEnvVars()` режет значение по
|
||||
первому `=`, то есть секрет с base64-паддингом через env приедет битым. Там же
|
||||
при включении OIDC надо поправить имена переменных — в 2.17.5 разбирается
|
||||
`WAKAPI_OIDC_PROVIDERS_0_*`, а у нас закомментирован устаревший вариант, который
|
||||
просто молча игнорируется.
|
||||
|
||||
Готово, когда `docker inspect` контейнера wakapi не показывает ни salt, ни пароль
|
||||
SMTP, а дифф шаблона можно смотреть без `no_log`.
|
||||
|
||||
Механизм, версии и ссылки на код:
|
||||
[справка по файловым секретам](../drafts/secrets-file-support.md).
|
||||
Из декомпозиции `secrets-env-to-file` (2026-07-25).
|
||||
|
||||
Связано: files/wakapi, files/miniflux (образец), roles/secrets,
|
||||
[no-secrets-in-playbook-output](no-secrets-in-playbook-output.md).
|
||||
@@ -13,6 +13,8 @@ gitea и т.д.) фактически все имеют root — изоляци
|
||||
тем — лучше через socket-proxy). За `primary_user` группу оставить для ручных
|
||||
операций (`inv ssh`).
|
||||
|
||||
Из sec-ревью (fable, 2026-07-13). Усиливает риск из [secrets-env-to-file](secrets-env-to-file.md).
|
||||
Из sec-ревью (fable, 2026-07-13). Усиливает риск из серии `secrets-file-*`
|
||||
(секреты в `environment:` контейнеров): членство в группе `docker` — как раз тот
|
||||
доступ, которым такие переменные и вычитываются.
|
||||
|
||||
Связано: все playbook-*.yml (owner_extra_groups), roles/owner, playbook-docker.yml.
|
||||
|
||||
@@ -39,8 +39,9 @@ enforcement-слой применяет снаружи. Что смена хра
|
||||
случайного дампа, от копии `~` в чужих руках, или от агента. Ответы ведут к разным
|
||||
вариантам, а третий не решается на этом уровне вообще.
|
||||
|
||||
Смежное: [secrets-env-to-file](secrets-env-to-file.md) — тот же вопрос слоем ниже
|
||||
(секреты в `environment:` контейнеров). Отдельно не заведена, но обсуждалась,
|
||||
Смежное: серия `secrets-file-*` — тот же вопрос слоем ниже (секреты в
|
||||
`environment:` контейнеров), разбор механизмов в
|
||||
[справке](../drafts/secrets-file-support.md). Отдельно не заведена, но обсуждалась,
|
||||
идея скоупинга: вернуть `group_vars/all/secrets.yml` в `vars_files`, оставив
|
||||
`main.yml` в `group_vars` — это режет потолок утечки с 53 переменных до ~11, ценой
|
||||
возврата boilerplate в плейбуки.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Файловые секреты: что умеет каждое приложение
|
||||
|
||||
Дата: 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](https://github.com/outline/outline/pull/11906),
|
||||
merged 2026-03-30), в v1.9.0 переписано на ленивый Proxy (PR
|
||||
[#12889](https://github.com/outline/outline/pull/12889)). Проверено бисекцией:
|
||||
в v1.6.1 механизма нет, в v1.7.0 есть.
|
||||
- Конвенция описана в
|
||||
[.env.sample](https://github.com/outline/outline/blob/v1.9.2/.env.sample)
|
||||
строки 4–18, включая правило приоритета: заданы обе — побеждает прямая
|
||||
переменная.
|
||||
- Пароль БД: `DATABASE_URL_FILE` выносит **весь** URL. Чтобы вынести только
|
||||
пароль, нужны раздельные `DATABASE_HOST/PORT/NAME/USER` + `DATABASE_PASSWORD`,
|
||||
они взаимоисключимы с `DATABASE_URL` через `@CannotUseWith`
|
||||
([env.ts L95–L143](https://github.com/outline/outline/blob/v1.9.2/server/env.ts)).
|
||||
- Сайдкар `postgres:16.3` — штатный `POSTGRES_PASSWORD_FILE`.
|
||||
|
||||
### wakapi `2.17.5`
|
||||
|
||||
Два независимых пути.
|
||||
|
||||
- `loadSecretFiles()`
|
||||
([config.go L829](https://github.com/muety/wakapi/blob/2.17.5/config/config.go))
|
||||
— generic: проходит по всему окружению, для любой переменной с суффиксом
|
||||
`_FILE` читает файл, `TrimSpace`, кладёт в базовое имя, файловую снимает.
|
||||
Заданы обе — процесс падает с «both environment variables are set».
|
||||
- Порядок в `Load()`: `loadSecretFiles()` → `renameEnvVars()` → `configor.Load`,
|
||||
поэтому `_FILE` работает и для OIDC-переменных в configor-формате.
|
||||
- История: PR [#679](https://github.com/muety/wakapi/pull/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-блоке (не живой баг, но при
|
||||
включении выстрелит):
|
||||
|
||||
1. Имена `WAKAPI_OIDC_PROVIDER_CLIENT_ID` устарели — в 2.17.5 разбирается
|
||||
`WAKAPI_OIDC_PROVIDERS_(\d+)_([A-Z_]+)`, старые молча игнорируются.
|
||||
2. `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](https://github.com/go-gitea/gitea/blob/v1.27.0/modules/setting/config_env.go),
|
||||
PR [#24832](https://github.com/go-gitea/gitea/pull/24832), с 1.20).
|
||||
|
||||
Оговорка, которая режет пользу: `environment-to-ini` **записывает содержимое
|
||||
файла в `app.ini` открытым текстом**, то есть секрет всё равно оседает на
|
||||
хостовом диске в `<data_dir>/gitea/conf/app.ini`. Открытая проблема апстрима
|
||||
[#35316](https://github.com/go-gitea/gitea/issues/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](https://github.com/gramps-project/gramps-web-api/blob/v3.18.0/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](https://github.com/gramps-project/gramps-web-api/blob/v3.18.0/gramps_webapi/app.py)).
|
||||
Обходной путь: `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](https://github.com/authelia/authelia/blob/v4.39.20/internal/configuration/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-фильтр](https://www.authelia.com/configuration/methods/files/)
|
||||
(`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](https://github.com/chrisvel/tududi/blob/v1.2.4/backend/config/config.js),
|
||||
[providerConfig.js L82](https://github.com/chrisvel/tududi/blob/v1.2.4/backend/modules/oidc/providerConfig.js),
|
||||
[service.js L20](https://github.com/chrisvel/tududi/blob/v1.2.4/backend/modules/ai-assistant/service.js)).
|
||||
Греп по репозиторию на теге даёт только `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](https://github.com/meilisearch/meilisearch/blob/v1.20.0/crates/meilisearch/src/option.rs)).
|
||||
Апстрим отклонил запрос —
|
||||
[discussion #201](https://github.com/orgs/meilisearch/discussions/201), 2023.
|
||||
Клиентская сторона (`wanderer_db`, `wanderer_web`) тоже читает `os.Getenv`.
|
||||
- `POCKETBASE_ENCRYPTION_KEY` — это **не** штатный механизм pocketbase
|
||||
(`--encryptionEnv`), а собственная переменная wanderer, читается `os.Getenv` в
|
||||
четырёх местах
|
||||
([db/main.go](https://github.com/Flomp/wanderer/blob/v0.18.3/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`.
|
||||
Reference in New Issue
Block a user