Backlog: security-ревью (fable) — 13 находок по приоритетам

This commit is contained in:
av
2026-07-13 19:47:51 +03:00
parent f110cf7208
commit b60e55ec5e
15 changed files with 240 additions and 19 deletions
+18 -5
View File
@@ -7,29 +7,42 @@
переезжает в ADR/спеку и пункт беклога удаляется.
Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку.
Пункты с пометкой _(ансибл-ревью)_ пришли из ревью плейбуков
[`docs/drafts/ansible-review.md`](../drafts/ansible-review.md) (2026-05-25);
_(tududi)_ — перенесены из инбокса задач в tududi (проект Pet Project Server).
Источники пунктов помечены:
_(ансибл-ревью)_ — ревью плейбуков [`docs/drafts/ansible-review.md`](../drafts/ansible-review.md) (2026-05-25);
_(tududi)_ — перенос из инбокса задач tududi (проект Pet Project Server);
_(sec-ревью)_ — три независимых security-ревью на модели fable (2026-07-13).
Драфты `timeweb.md` и `timeweb-migration-log.md` — исторические логи уже
завершённой миграции в Timeweb (cutover 2026-05-23), в беклог не входят.
## Высокий
- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata на restart-loop/unhealthy
- [Закрыть admin API Caddy](caddy-admin-api-localhost.md) — `admin :2019` слушает всю docker-сеть; любой контейнер может переписать маршрутизацию и снять forward_auth _(sec-ревью)_
- [Убрать группу `docker` у сервисных пользователей](service-users-docker-group.md) — `docker` = root на хосте; ~все app-аккаунты фактически root, изоляция «юзер на сервис» обнуляется _(sec-ревью)_
- [Изолировать/запаролить сессионный redis Authelia](authelia-redis-isolation.md) — стор сессий SSO в общей сети без пароля; запись в него = hijack SSO всех сервисов _(sec-ревью)_
- [Запретить SSH-вход под root](ssh-disable-root-login.md) — `permit_root_login: yes` на открытом 22; root — цель брутфорса, компрометация ключа = uid 0 _(sec-ревью)_
- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata
- [Рестарт контейнеров через handlers](ansible-handlers-restart.md) — `state: restarted` выполняется безусловно на каждом прогоне (лишний downtime), нет ни одного handler _(ансибл-ревью)_
## Средний
- [Секреты через `*_FILE`, а не inline `environment:`](secrets-env-to-file.md) — outline/wakapi/tududi/gitea/gramps/tuwunel держат секреты в env; видны в `docker inspect` и `/proc` _(sec-ревью)_
- [Захардить мониторинг-стек](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-ревью)_
- [Понизить log level Authelia до info](authelia-log-level.md) — `debug` в проде утекает детали аутентификации и OIDC через `docker logs`/dozzle _(sec-ревью)_
- [Включить автообновления безопасности](security-autoupdates.md) — патчи openssh/libssl только вручную; окна в недели между CVE и фиксом _(sec-ревью)_
- [Gitea runner on-demand в Yandex Cloud](gitea-runner-on-demand.md) — раннер активен только во время сборки; webhook→Cloud Function стартует ВМ, probe/decide гасят по idle; экономия ~95%
- [Синхронизация общих Ansible-ролей](shared-roles-sync.md) — `owner`/`eget`/`secrets` дублируются между репозиториями и дрейфуют; канон в ansible-shared + rsync-таски invoke
- [Уведомления о размере приложений и диске](disk-usage-alerts.md) — алерты на заполнение диска и аномальный рост data-томов; сервер ресурсно ограничен _(tududi)_
- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_
- [Точечные фиксы идемпотентности и баг имени play](ansible-quick-fixes.md) — быстрые правки: имя play в wanderer, `changed_when` в netdata/eget, backup-targets через template _(ансибл-ревью)_
- [Аудит безопасности сервера](security-audit.md) — разовый проход по экспозиции, Authelia, правам пользователей и секретам; находки дробятся в беклог _(tududi)_
## Низкий
- [Мелкий хардненинг Caddy](caddy-hardening-nits.md) — HSTS-заголовок, убрать лишний `NET_ADMIN`, убрать leftover-листенер `:29999` _(sec-ревью)_
- [Мелкий хардненинг контейнеров](container-hardening-nits.md) — `no-new-privileges`, non-root `user:`, homepage off `:latest`, соглашение о `127.0.0.1:` портах _(sec-ревью)_
- [Унифицировать ACL техпанелей Authelia](authelia-acl-unify.md) — rssbridge (SSRF) и dashboard за `one_factor` без subject; свести к `group:admins` + `two_factor` _(sec-ревью)_
- [`vars_files` → `group_vars/all/`](ansible-group-vars.md) — убрать повторяющийся boilerplate `vars_files` из всех плейбуков _(ансибл-ревью)_
- [Причесать роль `owner` под конвенции](ansible-owner-role-cleanup.md) — `assert` вместо `fail`+`when`, `loop` вместо `with_*`, добавить `meta`/README _(ансибл-ревью)_
- [Инвентарь: `host_vars`, группы, точечный `become`](ansible-inventory-hostvars.md) — хост-специфику в `host_vars/server.yml`, хост в именованную группу, глобальный root → точечный become _(ансибл-ревью)_
+15
View File
@@ -0,0 +1,15 @@
# Унифицировать ACL техпанелей Authelia
**Приоритет:** низкий
В `files/authelia/configuration.template.yml` `rssbridge` и `start` (dashboard)
закрыты `one_factor` вообще без `subject`, тогда как
netdata/dozzle/goaccess/bifrost/wanderbase/remembos — `two_factor` +
`group:admins`. rssbridge умеет ходить по произвольным URL (SSRF-поверхность) и при
этом живёт за одним фактором, доступен любому пользователю из `users.yml`. Для
одного пользователя риск невелик, но логичнее свести техпанели к `group:admins` +
`two_factor`, а `one_factor` без субъекта оставить только там, где это осознанно.
Из sec-ревью (fable, 2026-07-13).
Связано: files/authelia/configuration.template.yml.
+13
View File
@@ -0,0 +1,13 @@
# Понизить log level Authelia с debug до info
**Приоритет:** средний
`files/authelia/configuration.template.yml``log.level: "debug"`. Debug-логи
Authelia содержат детали запросов аутентификации, имена пользователей, заголовки и
внутренние подробности OIDC-потоков — они не нужны в обычной эксплуатации и
расширяют поверхность утечки через `docker logs`/dozzle (который сам открыт
админам). Поставить `info` (или `warn`), debug включать точечно при отладке.
Из sec-ревью (fable, 2026-07-13).
Связано: files/authelia/configuration.template.yml.
+15
View File
@@ -0,0 +1,15 @@
# Изолировать/запаролить сессионный redis Authelia
**Приоритет:** высокий
`authelia_redis` — стор сессий SSO — живёт в общей `monitoring_network` без пароля
(`files/authelia/docker-compose.template.yml`), а в той же сети сидят transcriber,
gramps_redis, outline_redis/postgres, miniflux_postgres, netdata. Компрометация
любого из них даёт запись в стор сессий Authelia → hijack SSO для всех сервисов.
Вынести пару authelia_app+authelia_redis в собственную internal-сеть (как уже
сделано у gramps/outline/miniflux с их `*_network`) и/или включить `requirepass`;
из `monitoring_network` redis убрать, если netdata его реально не скрейпит.
Из sec-ревью (fable, 2026-07-13).
Связано: files/authelia/docker-compose.template.yml, playbook-docker.yml.
+16
View File
@@ -0,0 +1,16 @@
# Закрыть admin API Caddy (localhost вместо всей сети)
**Приоритет:** высокий
`files/caddyproxy/Caddyfile.template:8``admin :2019`: admin API слушает на всех
интерфейсах контейнера, а в `web_proxy_network` сидят все приложения. Любой
скомпрометированный контейнер (rssbridge, tududi, calibre, ...) может POST-ом в
`http://caddyproxy:2019/config/` переписать маршрутизацию, снять `forward_auth`,
перехватить трафик или сертификаты. Порт реально нужен только netdata для метрик
(`files/netdata/go.d/prometheus.conf`). Фикс: вернуть `admin localhost:2019`, а
метрики отдать отдельным site-блоком `:2019 { metrics }` — netdata продолжит
скрейпить, admin API станет недоступен из сети.
Из sec-ревью (fable, 2026-07-13).
Связано: files/caddyproxy/Caddyfile.template, files/netdata/go.d/prometheus.conf.
+19
View File
@@ -0,0 +1,19 @@
# Мелкий хардненинг Caddy
**Приоритет:** низкий
- **HSTS**: заголовок `Strict-Transport-Security` не выставляется нигде в
`files/caddyproxy/Caddyfile.template`. Одна строка `header
Strict-Transport-Security "max-age=31536000"` в общем сниппете (рядом с
`access_log`) закрывает downgrade/SSL-strip для всех поддоменов разом.
- **Лишний `NET_ADMIN`** (`files/caddyproxy/docker-compose.template.yml`) —
reverse-proxy эта capability не нужна (требуется лишь для тюнинга UDP-буферов
HTTP/3, работает и без неё с warning). Убрать у edge-контейнера, торчащего в
интернет.
- **Leftover-листенер `:29999`** в `Caddyfile.template` (site `status.vakhrushev.me,
:29999`) — наследие прямого доступа к netdata; убрать, оставив только доменный
site-блок.
Из sec-ревью (fable, 2026-07-13).
Связано: files/caddyproxy/Caddyfile.template, files/caddyproxy/docker-compose.template.yml.
+17
View File
@@ -0,0 +1,17 @@
# Закрыть calibre-web forward-auth и проверить дефолтные креды
**Приоритет:** средний
`calibre.vakhrushev.me` проксируется на `calibre_web_app:8083` без блока
`forward_auth` (`files/caddyproxy/Caddyfile.template`), правила на этот домен нет и
в ACL Authelia — защита целиком на встроенной аутентификации calibre-web, которая
из коробки идёт с `admin`/`admin123`. Если пароль не сменён вручную, панель
управления библиотекой доступна в интернете с дефолтными кредами. Либо закрыть
домен `forward_auth` + правилом `group:admins`, либо подтвердить смену пароля
админа. Заодно перепроверить memos — не отключена ли публичная регистрация
первым-админом (`files/memos/docker-compose.template.yml`).
Из sec-ревью (fable, 2026-07-13).
Связано: files/caddyproxy/Caddyfile.template, files/authelia/configuration.template.yml,
files/calibre, files/memos.
+22
View File
@@ -0,0 +1,22 @@
# Мелкий хардненинг контейнеров
**Приоритет:** низкий
- **`no-new-privileges`**: ни один compose-шаблон не задаёт `security_opt:
["no-new-privileges:true"]`. Добавить всем — одна строка, режет эскалацию через
setuid-бинарники в образах.
- **non-root `user:`**: без него работают caddyproxy, gramps (данные в
`/root/.gramps`), rssbridge, dozzle, homepage и valkey/postgres-сайдкары gramps.
Дотянуть там, где образ позволяет (rssbridge, valkey) — продолжение уже принятой
в проекте практики.
- **homepage на `:latest`** (`vars/homepage.yml`) — плавающий тег + ночной `docker
image prune` может подтянуть другое содержимое без отката. Перевести на
иммутабельный `<sha>-<timestamp>` по образцу transcriber
(`vars/transcriber.images.yml`).
- **Соглашение о портах**: зафиксировать в AGENTS.md, что новые `ports:` — только
с `127.0.0.1:`. Docker публикует порты мимо UFW, поэтому дисциплина
localhost-биндинга — единственный контроль (сейчас соблюдается: apprise, wanderer).
Из sec-ревью (fable, 2026-07-13).
Связано: все files/*/docker-compose*.template.yml, vars/homepage.yml, AGENTS.md.
+25
View File
@@ -0,0 +1,25 @@
# Захардить мониторинг-стек (netdata / dozzle / goaccess)
**Приоритет:** средний
Мониторинг-контейнеры — самые привилегированные на хосте и при этом торчат в общую
`web_proxy_network` рядом с приложениями:
- **netdata** (`files/netdata/docker-compose.template.yml`) — `SYS_ADMIN` +
`SYS_PTRACE` + `apparmor:unconfined`, плюс весь `/` и docker.sock: фактически
root-контейнер. Для метрик обычно хватает `SYS_PTRACE`; `SYS_ADMIN` и
`apparmor:unconfined` попробовать убрать (netdata деградирует мягко).
- **dozzle** (`files/dozzle/docker-compose.template.yml`) — RW docker.sock +
`DOZZLE_AUTH_PROVIDER: forward-proxy` (верит заголовку `Remote-User` от кого
угодно). Любой контейнер в `web_proxy_network` может напрямую сходить на
`dozzle_app:8080` с поддельным `Remote-User` и читать логи всех контейнеров (в
логах бывают токены/ссылки).
- docker.sock обоим пустить через `tecnativa/docker-socket-proxy` (только нужные
GET), напрямую не монтировать.
- перенести netdata/dozzle/goaccess в существующую `monitoring_network`
(`playbook-docker.yml`), а Caddy подключить к обеим сетям — уводит их из общей
сети с приложениями и устраняет lateral movement.
Из sec-ревью (fable, 2026-07-13).
Связано: files/netdata, files/dozzle, files/goaccess, files/caddyproxy, playbook-docker.yml.
+16
View File
@@ -0,0 +1,16 @@
# Ограничить эндпоинт `/metrics` Miniflux
**Приоритет:** средний
В `files/miniflux/docker-compose.template.yml` заданы `METRICS_COLLECTOR=1` и
`METRICS_ALLOWED_NETWORKS=0.0.0.0/0`, а Caddy проксирует весь
`miniflux.vakhrushev.me` без ограничения пути. За прокси Miniflux видит приватный
IP контейнера Caddy (попадает в `0.0.0.0/0`), поэтому
`https://miniflux.vakhrushev.me/metrics` отдаётся публично без аутентификации —
утечка эксплуатационной статистики. Сузить `METRICS_ALLOWED_NETWORKS` до подсети
`monitoring_network` (netdata скрейпит по внутреннему имени) или заблокировать
путь `/metrics` в Caddy.
Из sec-ревью (fable, 2026-07-13).
Связано: files/miniflux/docker-compose.template.yml, files/caddyproxy/Caddyfile.template.
+17
View File
@@ -0,0 +1,17 @@
# Секреты приложений через `*_FILE`, а не inline `environment:`
**Приоритет:** средний
miniflux сделан правильно — секреты через `*_FILE` + монтирование каталога
secrets `:ro` (`files/miniflux/docker-compose.template.yml`). Но outline
(DATABASE_URL с паролём, `SECRET_KEY`, `UTILS_SECRET`, OIDC secret, SMTP), wakapi
(salt, SMTP), tududi (session secret, OIDC secret, ключ LLM), gitea (SMTP),
gramps, tuwunel кладут секреты прямыми значениями в `environment:`. Такие
переменные читает любой член группы `docker`, любой процесс с доступом к
`/proc/<pid>/environ`, и они попадают в `docker inspect`. Где приложение умеет
`*_FILE`/конфиг-файл — постепенно перевести по образцу miniflux.
Из sec-ревью (fable, 2026-07-13). Связка с [service-users-docker-group](service-users-docker-group.md).
Связано: files/outline, files/wakapi, files/tududi, files/gitea, files/gramps,
files/tuwunel, files/miniflux (образец).
-14
View File
@@ -1,14 +0,0 @@
# Аудит безопасности сервера
**Приоритет:** средний
Провести аудит безопасности сервера и конфигураций (Fable / `/security-review`):
поверхность экспозиции наружу (Caddy, открытые порты, UFW), аутентификация и
границы Authelia, права системных пользователей приложений, обращение с
секретами (vault, env-файлы, docker socket), настройки docker-compose. Разовый
проход; найденное дробится на отдельные пункты беклога.
Из tududi (проект Pet Project Server).
Связано: playbook-ufw.yml, playbook-caddyproxy.yml, playbook-authelia.yml,
roles/owner, roles/secrets.
+13
View File
@@ -0,0 +1,13 @@
# Включить автообновления безопасности
**Приоритет:** средний
`playbook-system.yml:35``security_autoupdate_enabled: "no"`: хост с открытыми в
интернет 22/80/443/2222 получает патчи только при ручном запуске
`playbook-upgrade.yml`, т.е. окна в недели между публикацией CVE и патчем
openssh/libssl. Соразмерный фикс — включить unattended-upgrades только для
security-репозитория (одна переменная роли), без автоматических перезагрузок.
Из sec-ревью (fable, 2026-07-13).
Связано: playbook-system.yml, galaxy.roles/geerlingguy.security, playbook-upgrade.yml.
@@ -0,0 +1,18 @@
# Убрать группу `docker` у сервисных пользователей
**Приоритет:** высокий
Все приложения заводят системного пользователя с `owner_extra_groups: ["docker"]`
(во всех `playbook-*.yml`), но docker.sock реально монтируют только netdata и
dozzle. Членство в группе `docker` эквивалентно root на хосте (`docker run -v
/:/host`), поэтому выделенные «непривилегированные» аккаунты (authelia, outline,
gitea и т.д.) фактически все имеют root — изоляция «пользователь на сервис», ради
которой их и заводили, обнуляется. Контейнерами рулит Ansible от root (become),
сервисам группа не нужна. Убрать `docker` из `owner_extra_groups` у всех, кроме
тех, кому он действительно нужен для самостоятельного управления своим compose (и
тем — лучше через socket-proxy). За `primary_user` группу оставить для ручных
операций (`inv ssh`).
Из sec-ревью (fable, 2026-07-13). Усиливает риск из [secrets-env-to-file](secrets-env-to-file.md).
Связано: все playbook-*.yml (owner_extra_groups), roles/owner, playbook-docker.yml.
+16
View File
@@ -0,0 +1,16 @@
# Запретить SSH-вход под root
**Приоритет:** высокий
`playbook-system.yml:34` переопределяет дефолт роли на
`security_ssh_permit_root_login: "yes"`. Парольная аутентификация выключена (вход
только по ключу), но root — единственный аккаунт, который брутфорсят все боты на
открытом 22/tcp, и компрометация ключа сразу даёт uid 0 без следа sudo. Деплой
идёт под отдельным непривилегированным пользователем с become (`production.yml`),
root-логин для Ansible не нужен. Поставить `"no"`, либо `prohibit-password`, если
root-ключ нужен для аварийного доступа (то же поведение по ключу, но явный запрет
пароля на будущее).
Из sec-ревью (fable, 2026-07-13).
Связано: playbook-system.yml, production.yml, galaxy.roles/geerlingguy.security.