diff --git a/docs/backlog/README.md b/docs/backlog/README.md index f26d50a..54df4a3 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -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 _(ансибл-ревью)_ diff --git a/docs/backlog/authelia-acl-unify.md b/docs/backlog/authelia-acl-unify.md new file mode 100644 index 0000000..c4815d1 --- /dev/null +++ b/docs/backlog/authelia-acl-unify.md @@ -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. diff --git a/docs/backlog/authelia-log-level.md b/docs/backlog/authelia-log-level.md new file mode 100644 index 0000000..95a50ad --- /dev/null +++ b/docs/backlog/authelia-log-level.md @@ -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. diff --git a/docs/backlog/authelia-redis-isolation.md b/docs/backlog/authelia-redis-isolation.md new file mode 100644 index 0000000..42108c5 --- /dev/null +++ b/docs/backlog/authelia-redis-isolation.md @@ -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. diff --git a/docs/backlog/caddy-admin-api-localhost.md b/docs/backlog/caddy-admin-api-localhost.md new file mode 100644 index 0000000..88e1c9b --- /dev/null +++ b/docs/backlog/caddy-admin-api-localhost.md @@ -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. diff --git a/docs/backlog/caddy-hardening-nits.md b/docs/backlog/caddy-hardening-nits.md new file mode 100644 index 0000000..80b475b --- /dev/null +++ b/docs/backlog/caddy-hardening-nits.md @@ -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. diff --git a/docs/backlog/calibre-forward-auth.md b/docs/backlog/calibre-forward-auth.md new file mode 100644 index 0000000..92ae968 --- /dev/null +++ b/docs/backlog/calibre-forward-auth.md @@ -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. diff --git a/docs/backlog/container-hardening-nits.md b/docs/backlog/container-hardening-nits.md new file mode 100644 index 0000000..e220bcc --- /dev/null +++ b/docs/backlog/container-hardening-nits.md @@ -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` может подтянуть другое содержимое без отката. Перевести на + иммутабельный `-` по образцу 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. diff --git a/docs/backlog/harden-monitoring-stack.md b/docs/backlog/harden-monitoring-stack.md new file mode 100644 index 0000000..67bb11d --- /dev/null +++ b/docs/backlog/harden-monitoring-stack.md @@ -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. diff --git a/docs/backlog/miniflux-metrics-restrict.md b/docs/backlog/miniflux-metrics-restrict.md new file mode 100644 index 0000000..89d5277 --- /dev/null +++ b/docs/backlog/miniflux-metrics-restrict.md @@ -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. diff --git a/docs/backlog/secrets-env-to-file.md b/docs/backlog/secrets-env-to-file.md new file mode 100644 index 0000000..d3a1681 --- /dev/null +++ b/docs/backlog/secrets-env-to-file.md @@ -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//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 (образец). diff --git a/docs/backlog/security-audit.md b/docs/backlog/security-audit.md deleted file mode 100644 index 763ac80..0000000 --- a/docs/backlog/security-audit.md +++ /dev/null @@ -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. diff --git a/docs/backlog/security-autoupdates.md b/docs/backlog/security-autoupdates.md new file mode 100644 index 0000000..9a4abfa --- /dev/null +++ b/docs/backlog/security-autoupdates.md @@ -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. diff --git a/docs/backlog/service-users-docker-group.md b/docs/backlog/service-users-docker-group.md new file mode 100644 index 0000000..b1930ce --- /dev/null +++ b/docs/backlog/service-users-docker-group.md @@ -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. diff --git a/docs/backlog/ssh-disable-root-login.md b/docs/backlog/ssh-disable-root-login.md new file mode 100644 index 0000000..2dcf0eb --- /dev/null +++ b/docs/backlog/ssh-disable-root-login.md @@ -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.