Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8cd5cab6
|
||
|
|
b60e55ec5e
|
||
|
|
f110cf7208
|
||
|
|
30358de9d9
|
||
|
|
a41f6bca8e
|
@@ -0,0 +1,51 @@
|
|||||||
|
# Беклог
|
||||||
|
|
||||||
|
Единый список будущих задач по серверу: то, что уже решили сделать, и идеи,
|
||||||
|
которые ещё надо обдумать. Это **источник истины по беклогу** — одна задача =
|
||||||
|
один файл в этом каталоге. Не план реализации: детальные проработки живут в
|
||||||
|
[`docs/drafts`](../drafts) (беклог даёт сводку и ссылку), а реализованное
|
||||||
|
переезжает в ADR/спеку и пункт беклога удаляется.
|
||||||
|
|
||||||
|
Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку.
|
||||||
|
Источники пунктов помечены:
|
||||||
|
_(ансибл-ревью)_ — ревью плейбуков [`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), в беклог не входят.
|
||||||
|
|
||||||
|
## Высокий
|
||||||
|
|
||||||
|
- [Закрыть 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-ревью)_
|
||||||
|
- [Алерты на проблемные контейнеры](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-ревью)_
|
||||||
|
- [Запретить SSH-вход под root](ssh-disable-root-login.md) — `PermitRootLogin yes` пускает root по ключу; наименьшие привилегии + defense-in-depth (пароль уже off) _(sec-ревью)_
|
||||||
|
- [Убрать устаревший инвентарь после миграции](stale-inventory-cleanup.md) — `production.yml` указывает на мёртвый YC-хост, живой — `timeweb.yml`; имена инвертированы
|
||||||
|
- [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 _(ансибл-ревью)_
|
||||||
|
|
||||||
|
## Низкий
|
||||||
|
|
||||||
|
- [Мелкий хардненинг 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 _(ансибл-ревью)_
|
||||||
|
- [Фоновая зачистка стиля и конфигурации](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_
|
||||||
|
- [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Composable-роль `backup`
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Бэкап — самый чистый шов для extraction: `gobackup.yml` + `backup.sh` +
|
||||||
|
`backup-targets` + интеграция с restic одинаковы у всех сервисов, различается
|
||||||
|
только список целей. Вынести в роль `backup` с параметром «список targets» — она
|
||||||
|
не трогает индивидуальность сервиса. Это правильный размер абстракции (как уже
|
||||||
|
сделанный `owner`), а **не** мега-роль `docker_app` (та отклонена осознанно:
|
||||||
|
приложения реально разные, catch-all обрастает `when:`). Обкатать на одном
|
||||||
|
сервисе, затем раскатать.
|
||||||
|
|
||||||
|
Полный контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §1.
|
||||||
|
|
||||||
|
Связано: roles/owner (образец), files/<app>/gobackup.template.yml,
|
||||||
|
files/<app>/backup.template.sh, playbook-backups.yml,
|
||||||
|
feedback_per_app_playbook_duplication.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# `vars_files` → `group_vars/all/`
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
В каждом плейбуке повторяется boilerplate:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vars_files:
|
||||||
|
- vars/secrets.yml
|
||||||
|
- vars/vars.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Ansible автоматически подхватывает `group_vars/all.yml` и `group_vars/all/secrets.yml`
|
||||||
|
(vault) для группы `all`. Перенос `vars/vars.yml` → `group_vars/all/main.yml` и
|
||||||
|
`vars/secrets.yml` → `group_vars/all/vault.yml` убирает `vars_files` из всех
|
||||||
|
плейбуков. Низкий риск, адаптируется по одному плейбуку за раз. Учесть
|
||||||
|
`.crushignore`/проверку шифрования vault (маска `*secrets.yml`) и pre-commit-хук
|
||||||
|
проверки шифрования при переименовании.
|
||||||
|
|
||||||
|
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §2.
|
||||||
|
|
||||||
|
Связано: все playbook-*.yml, vars/secrets.yml, vars/vars.yml, lefthook.yml.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Рестарт контейнеров через handlers, а не безусловно
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Ни в одном плейбуке нет `handlers:`. Вместо этого задача `state: restarted`
|
||||||
|
выполняется **всегда** — рестартит контейнер на каждом прогоне даже без
|
||||||
|
изменений (`playbook-caddyproxy.yml:106`, `playbook-netdata.yml:143`,
|
||||||
|
`playbook-authelia.yml:92`): не идемпотентно, лишний downtime. В
|
||||||
|
`playbook-gitea.yml` рестарта нет вовсе — несогласованность. Канонический
|
||||||
|
паттерн: шаблон конфига `notify`-ит handler, который делает
|
||||||
|
`docker_compose_v2: state: restarted` только при реальном изменении. Внедряется
|
||||||
|
инкрементально, по одному сервису. Заодно убрать мёртвый
|
||||||
|
`docker_compose_file_result` в `playbook-memos.yml:76` (регистрируется, нигде не
|
||||||
|
используется — задумывался под `when`/`notify`).
|
||||||
|
|
||||||
|
Топ-приоритет ансибл-ревью. Полный контекст:
|
||||||
|
[docs/drafts/ansible-review.md](../drafts/ansible-review.md) §3.
|
||||||
|
|
||||||
|
Связано: playbook-caddyproxy.yml, playbook-netdata.yml, playbook-authelia.yml,
|
||||||
|
playbook-gitea.yml, playbook-memos.yml.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Инвентарь: `host_vars`, именованные группы, точечный `become`
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
- **`production.yml` и `timeweb.yml`** оба объявляют хост `server` под
|
||||||
|
`ungrouped:`, хост-специфичные данные (`application_dir`,
|
||||||
|
`mount_external_storage`, `ansible_host`, `ansible_user`) вписаны инлайн.
|
||||||
|
Конвенциональнее — `host_vars/server.yml` и хост в именованной группе. Два
|
||||||
|
инвентаря с одинаковым именем хоста + `hosts: all` — ошибка `-i` молча уедет не
|
||||||
|
туда. (`timeweb.yml` — артефакт завершённой миграции, заодно решить, нужен ли
|
||||||
|
он ещё.)
|
||||||
|
- `ansible_become: true` глобально — всё бежит под root. Для личного сервера
|
||||||
|
прагматично; точечный `become`/`become_user` ближе к наименьшим привилегиям, но
|
||||||
|
это низкий приоритет.
|
||||||
|
|
||||||
|
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §6.
|
||||||
|
|
||||||
|
Связано: production.yml, timeweb.yml.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Причесать роль `owner` под конвенции
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Роль `owner` разошлась по стилю с `eget`/`secrets`:
|
||||||
|
|
||||||
|
- **`roles/owner/tasks/main.yml:2-10`** — валидация аргументов через `fail` +
|
||||||
|
`when`, причём две задачи с **идентичным именем**. `eget` для того же делает
|
||||||
|
`assert` (`roles/eget/tasks/main.yml:15`). Привести к одному стилю — `assert`
|
||||||
|
либо декларативный `meta/argument_specs.yml`.
|
||||||
|
- **`roles/owner/tasks/main.yml:32,53`** — устаревшие `with_items`/`with_dict`;
|
||||||
|
конвенция — `loop` (`loop: "{{ owner_ssh_keys }}"`,
|
||||||
|
`loop: "{{ owner_env_dict | dict2items }}"`).
|
||||||
|
- У `owner` нет `meta/main.yml` и README, тогда как у `eget` и `secrets` есть.
|
||||||
|
- Имена задач с точкой на конце (`"Prepare env variables."`) — ansible-lint в
|
||||||
|
строгом профиле это ловит.
|
||||||
|
|
||||||
|
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §5.
|
||||||
|
|
||||||
|
Связано: roles/owner, roles/eget (образец), roles/secrets.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Точечные фиксы идемпотентности и баг имени play
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Быстрые пойнтовые правки без структурных изменений:
|
||||||
|
|
||||||
|
- **`playbook-wanderer.yml:2`** — play назван `"Configure gramps application"`
|
||||||
|
при `app_name: "wanderer"` (копипаст из gramps). Поправить имя. _(баг)_
|
||||||
|
- **`playbook-netdata.yml:118-125`** — `changed_when: ...rc != 0` для read-only
|
||||||
|
запроса лишён смысла; должно быть `changed_when: false`. Лучше заменить
|
||||||
|
`shell: grep docker /etc/group` на модуль `ansible.builtin.getent` — уйдёт
|
||||||
|
`pipefail` и хрупкий парсинг.
|
||||||
|
- **`playbook-eget.yml:23-78`** — восемь `command` с `changed_when: false`, хотя
|
||||||
|
реально ставят/обновляют бинарники: прогон всегда «ok», теряется честность
|
||||||
|
`--diff`. Ставить через роль `eget` (она корректно проверяет версию) или через
|
||||||
|
проверку версии.
|
||||||
|
- **`playbook-memos.yml:57-67`** и аналоги — сборка `backup-targets` через
|
||||||
|
`lineinfile` в цикле не удаляет устаревшие строки при изменении списка; `mode:
|
||||||
|
"0750"` на файле-списке выглядит как copy-paste. Чище — `template`/`copy:
|
||||||
|
content` со всем списком.
|
||||||
|
|
||||||
|
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §4, §7.
|
||||||
|
|
||||||
|
Связано: playbook-wanderer.yml, playbook-netdata.yml, playbook-eget.yml,
|
||||||
|
playbook-memos.yml, roles/eget.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Фоновая зачистка стиля и конфигурации Ansible
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Косметика и мелочи конфигурации, копятся в один проход:
|
||||||
|
|
||||||
|
- **sudoers**: `playbook-backups.yml:52-59` правит `/etc/sudoers` через
|
||||||
|
`lineinfile`. Конвенция — отдельный файл в `/etc/sudoers.d/` (через
|
||||||
|
`copy`/`template` с `validate: visudo -cf %s`).
|
||||||
|
- **`.ansible-lint.yml`** содержит только `exclude_paths`, профиль не задан явно,
|
||||||
|
хотя AGENTS.md заявляет «профиль production». Прописать `profile: production`
|
||||||
|
либо поправить документацию.
|
||||||
|
- **`ansible.cfg`** минимален — добавить `stdout_callback = yaml`,
|
||||||
|
`interpreter_python = auto_silent`, `force_handlers = true` (последнее нужно
|
||||||
|
вместе с переходом на handlers).
|
||||||
|
- Несогласованные кавычки и пути (`'directory'` vs `"directory"`,
|
||||||
|
`src: "./files/..."` vs `src: "files/..."`, одинарные кавычки в
|
||||||
|
`playbook-all-setup.yml`).
|
||||||
|
- **`playbook-system.yml:24`** — `apt` без `cache_valid_time`, обновляет кэш
|
||||||
|
каждый прогон.
|
||||||
|
|
||||||
|
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §8.
|
||||||
|
|
||||||
|
Связано: playbook-backups.yml, .ansible-lint.yml, ansible.cfg,
|
||||||
|
playbook-system.yml, playbook-all-setup.yml.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Logrotate для логов бэкапов
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Логи бэкапов на сервере (вывод `backup-all.py` / restic) копятся без ротации —
|
||||||
|
со временем растут неограниченно. Настроить logrotate (отдельный файл в
|
||||||
|
`/etc/logrotate.d/` через `copy`/`template`) с разумным retention и сжатием.
|
||||||
|
Завести в `playbook-backups.yml` рядом с настройкой самих бэкапов.
|
||||||
|
|
||||||
|
Из tududi (проект Pet Project Server).
|
||||||
|
|
||||||
|
Связано: playbook-backups.yml, files/backups/backup-all.py,
|
||||||
|
project_restic_intelligent_tiering.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Алерты на проблемные контейнеры
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
wakapi однажды упал на миграциях и несколько дней крутился в restart-loop —
|
||||||
|
никто не узнал. Docker считал контейнер «running», пока процесс жив. Нужны два
|
||||||
|
слоя: (1) healthcheck + `start_period` в compose-шаблонах, чтобы Docker видел
|
||||||
|
реальное состояние (окно `start_period` даёт миграциям отработать); (2) алерты
|
||||||
|
через Netdata на restart-loop (счётчик перезапусков растёт) и на
|
||||||
|
`container_health_status != healthy` дольше M минут, канал нотификаций — один
|
||||||
|
(Telegram/ntfy — выбрать). restart policy оставляем `unless-stopped` (алерт +
|
||||||
|
ручное решение вместо `on-failure`, который не встаёт после ребута). Опционально
|
||||||
|
позже — Uptime Kuma для внешнего HTTP-чека по публичным URL.
|
||||||
|
|
||||||
|
Полный план: [docs/drafts/alerts.md](../drafts/alerts.md).
|
||||||
|
|
||||||
|
Связано: playbook-netdata.yml, compose-шаблоны всех сервисов, project_server_specs.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Уведомления о размере приложений и заполнении диска
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Сервер ресурсно ограничен (диск небольшой, RAM ~80% на штатной нагрузке —
|
||||||
|
`project_server_specs`), а рост данных сейчас никак не сигнализируется: узнаёшь,
|
||||||
|
только когда место кончилось. Нужны алерты на (1) заполнение диска выше порога
|
||||||
|
(общий том + data-тома приложений) и (2) аномальный рост размера отдельного
|
||||||
|
приложения (лог/БД/кэш распух). Реализация — через Netdata (disk space collector
|
||||||
|
+ алерты в `health.d/*.conf`), тот же канал нотификаций, что и у алертов на
|
||||||
|
контейнеры. Пороги калибровать под фактический объём диска.
|
||||||
|
|
||||||
|
Отдельный сигнал от [container-alerts](container-alerts.md) (там — здоровье
|
||||||
|
контейнеров, здесь — ёмкость). Канал нотификаций общий.
|
||||||
|
|
||||||
|
Из tududi (проект Pet Project Server).
|
||||||
|
|
||||||
|
Связано: playbook-netdata.yml, container-alerts, project_server_specs.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Gitea runner on-demand в Yandex Cloud
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Self-hosted раннер Gitea Actions, активный только во время сборки. Сборок ~10/нед
|
||||||
|
по ~5 мин — ВМ 24/7 даёт утилизацию ~1% (≈$23/мес), on-demand — ≈$1/мес (экономия
|
||||||
|
~95%). Архитектура: push → Gitea webhook → Cloud Function (HMAC-валидация +
|
||||||
|
стейт-машина старта) → Compute API стартует ВМ → `act_runner` в docker забирает
|
||||||
|
джобу; на ВМ probe (телеметрия раз в 30с) + decide (решение раз в 1 мин) гасят ВМ
|
||||||
|
после idle-окна через Compute REST из metadata-токена. Три слоя страховки от
|
||||||
|
зависшей ВМ (soft idle-stop, probe-staleness, внешний алерт Cloud Monitoring на
|
||||||
|
uptime > 24ч). Плейбук `playbook-gitea-runner.yml` + набор invoke-тасков
|
||||||
|
(`runner-bootstrap`, `runner-deploy-function`, `runner-pl`, ...).
|
||||||
|
|
||||||
|
Дизайн проработан целиком (ресурсы YC, секреты, стоимость, принятые риски, план
|
||||||
|
внедрения из 11 шагов). Открытые вопросы: канал нотификаций, executor (docker),
|
||||||
|
webhook на PR. Полный дизайн:
|
||||||
|
[docs/drafts/gitea-runner-on-demand.md](../drafts/gitea-runner-on-demand.md).
|
||||||
|
|
||||||
|
Связано: playbook-gitea.yml, roles/owner, vars/secrets.yml, tasks.py.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 (образец).
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Синхронизация общих Ansible-ролей между репозиториями
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Серверы управляются изолированными репозиториями (pet-project-server, buckland,
|
||||||
|
далее — торрент-бокс). Кастомные роли (`owner`, `eget`, `secrets`) концептуально
|
||||||
|
общие, но физически продублированы и дрейфуют (`owner` в buckland стал мёртвым
|
||||||
|
кодом). Решение под pets-подход: канон ролей в `~/projects/private/ansible-shared/`,
|
||||||
|
каждый репозиторий держит закоммиченную копию (она же «пин версии») и
|
||||||
|
синхронизируется через rsync-таски invoke — `roles-pull` / `roles-push` /
|
||||||
|
`roles-status`. Подписка на роли явная и пер-серверная (`SYNCED_ROLES` в
|
||||||
|
`tasks.py`). Обновление роли на сервере — осознанный коммит с читаемым диффом, не
|
||||||
|
скрытый сайд-эффект. Предохранитель от дрейфа — предупреждение в lefthook.
|
||||||
|
Отвергнуты: общий `roles_path`, симлинки, submodule/subtree, galaxy-из-git,
|
||||||
|
монорепо (все ломают самодостаточность или вводят лишнюю петлю).
|
||||||
|
|
||||||
|
Подход обсуждён, реализация не начата. Полный дизайн + план внедрения:
|
||||||
|
[docs/drafts/shared-roles-sync.md](../drafts/shared-roles-sync.md).
|
||||||
|
|
||||||
|
Связано: roles/owner, roles/eget, roles/secrets, tasks.py, lefthook.yml,
|
||||||
|
project_isolated_repos_shared_roles, project_shared_invoke_command_surface.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Запретить SSH-вход под root
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
`playbook-system.yml:34` переопределяет дефолт роли на
|
||||||
|
`security_ssh_permit_root_login: "yes"` → `PermitRootLogin yes`. Это управляет
|
||||||
|
**только** тем, пускать ли root по SSH, и не имеет отношения к паролю: вход по
|
||||||
|
паролю уже запрещён отдельной директивой `PasswordAuthentication no` (дефолт роли
|
||||||
|
`security_ssh_password_authentication: "no"`, не переопределён). То есть брутфорса
|
||||||
|
пароля тут нет — остаётся то, что root **явно разрешён к входу по ключу**.
|
||||||
|
|
||||||
|
Почему всё равно стоит закрыть:
|
||||||
|
|
||||||
|
- **Наименьшие привилегии.** Ansible ходит под непривилегированным `major` с
|
||||||
|
`become` (`production.yml`/`timeweb.yml`), root-логин для деплоя не нужен вовсе.
|
||||||
|
- **Defense-in-depth.** Если `PasswordAuthentication` когда-нибудь вернётся в
|
||||||
|
`yes` (правка, `Match`-блок, чужая роль), `PermitRootLogin yes` тут же откроет
|
||||||
|
root по паролю. `prohibit-password` держит root key-only независимо.
|
||||||
|
|
||||||
|
Рекомендация: поставить `"no"` (root по SSH недоступен, деплой под `major` не
|
||||||
|
страдает). Если нужен аварийный вход root по ключу — `"prohibit-password"`.
|
||||||
|
|
||||||
|
Из sec-ревью (fable, 2026-07-13); мотивировка уточнена после проверки конфига.
|
||||||
|
|
||||||
|
Связано: playbook-system.yml, production.yml, timeweb.yml,
|
||||||
|
galaxy.roles/geerlingguy.security.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Убрать устаревший инвентарь после миграции на Timeweb
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
После переезда на Timeweb (cutover 2026-05-23) имена инвентарей инвертировались:
|
||||||
|
|
||||||
|
- `timeweb.yml` → `92.53.105.41` — **живой** хост, именно его использует `inv pl`
|
||||||
|
(`tasks.py:12` `HOSTS_FILE = "timeweb.yml"`).
|
||||||
|
- `production.yml` → `158.160.46.255` — **мёртвый** старый YC-хост (ВМ
|
||||||
|
остановлена). При этом AGENTS.md всё ещё называет `production.yml` рабочим
|
||||||
|
инвентарём.
|
||||||
|
|
||||||
|
Ловушка: любой прямой запуск `ansible-playbook -i production.yml ...` (как в
|
||||||
|
примерах AGENTS.md) уедет на несуществующий/чужой хост. Плюс `production.yml`
|
||||||
|
содержит `application_dir`/`mount_external_storage`, которых нет в `timeweb.yml` —
|
||||||
|
простой заменой файла эти переменные потеряются.
|
||||||
|
|
||||||
|
Сделать: сделать `production.yml` актуальным инвентарём Timeweb-хоста (перенести
|
||||||
|
недостающие host-vars), удалить `timeweb.yml`, поправить `HOSTS_FILE` в `tasks.py`
|
||||||
|
и примеры в AGENTS.md. Согласуется с задачей
|
||||||
|
[ansible-inventory-hostvars](ansible-inventory-hostvars.md) (host-специфику в
|
||||||
|
`host_vars/server.yml`, хост в именованную группу) — логично делать вместе.
|
||||||
|
|
||||||
|
Связано: production.yml, timeweb.yml, tasks.py, AGENTS.md,
|
||||||
|
docs/drafts/timeweb-migration-log.md, ansible-inventory-hostvars.
|
||||||
@@ -14,6 +14,10 @@ VARS_FILE = "vars/vars.yml"
|
|||||||
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
|
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Базовый набор: единый интерфейс команд во всех репозиториях ---
|
||||||
|
# (набор и сигнатуры команд общие; реализация может отличаться по месту)
|
||||||
|
|
||||||
|
|
||||||
def _yq(query: str) -> str:
|
def _yq(query: str) -> str:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["yq", query, HOSTS_FILE], capture_output=True, text=True, check=True
|
["yq", query, HOSTS_FILE], capture_output=True, text=True, check=True
|
||||||
@@ -29,25 +33,6 @@ def _remote_host() -> str:
|
|||||||
return _yq(".ungrouped.hosts.server.ansible_host")
|
return _yq(".ungrouped.hosts.server.ansible_host")
|
||||||
|
|
||||||
|
|
||||||
def _application_dir() -> str:
|
|
||||||
"""Чтение application_dir: сначала из inventory (override), затем из vars/vars.yml."""
|
|
||||||
inv_value = _yq('.ungrouped.hosts.server.application_dir // ""')
|
|
||||||
if inv_value:
|
|
||||||
return inv_value
|
|
||||||
result = subprocess.run(
|
|
||||||
["yq", ".application_dir", VARS_FILE],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
value = result.stdout.strip()
|
|
||||||
if not value or value == "null":
|
|
||||||
raise Exit(
|
|
||||||
f"application_dir не определён ни в inventory, ни в {VARS_FILE}", code=1
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _rest_args() -> list[str]:
|
def _rest_args() -> list[str]:
|
||||||
"""Возвращает аргументы после '--' из sys.argv"""
|
"""Возвращает аргументы после '--' из sys.argv"""
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +59,7 @@ def install_roles(ctx: Context) -> None:
|
|||||||
|
|
||||||
@task
|
@task
|
||||||
def pl(ctx: Context) -> None:
|
def pl(ctx: Context) -> None:
|
||||||
"""Запустить плейбуки по имени: inv pl -- gitea miniflux"""
|
"""Запустить плейбуки по имени: inv pl -- <name> [name ...]"""
|
||||||
names = _rest_args()
|
names = _rest_args()
|
||||||
if not names:
|
if not names:
|
||||||
raise Exit("Укажи хотя бы один плейбук: inv pl -- <name> [name ...]", code=1)
|
raise Exit("Укажи хотя бы один плейбук: inv pl -- <name> [name ...]", code=1)
|
||||||
@@ -88,34 +73,28 @@ def pl(ctx: Context) -> None:
|
|||||||
@task
|
@task
|
||||||
def ssh(ctx: Context) -> None:
|
def ssh(ctx: Context) -> None:
|
||||||
"""SSH на удалённый сервер"""
|
"""SSH на удалённый сервер"""
|
||||||
subprocess.run(f"ssh {_remote_user()}@{_remote_host()}", shell=True)
|
ctx.run(f"ssh {_remote_user()}@{_remote_host()}", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def zj(ctx: Context) -> None:
|
def zj(ctx: Context) -> None:
|
||||||
"""Запуск zellij на удаленном сервере"""
|
"""Запуск zellij на удалённом сервере"""
|
||||||
subprocess.run(
|
ctx.run(
|
||||||
f"ssh {_remote_user()}@{_remote_host()} -t zellij attach --create main",
|
f"ssh {_remote_user()}@{_remote_host()} -t zellij attach --create main",
|
||||||
shell=True,
|
pty=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@task(aliases=["login"])
|
@task
|
||||||
def login_as_app(ctx: Context, app: str) -> None:
|
def mc(ctx: Context) -> None:
|
||||||
"""SSH и переключиться на пользователя приложения: inv login gitea"""
|
"""Midnight Commander: слева локальная директория, справа удалённая домашняя"""
|
||||||
# sudo -i: login shell, -u: от имени пользователя
|
ctx.run(f"mc . sh://{_remote_user()}@{_remote_host()}", pty=True)
|
||||||
# bash -i: интерактивный режим (job control), -l: login (читает профиль)
|
|
||||||
app_dir = f"{_application_dir()}/{app}"
|
|
||||||
subprocess.run(
|
|
||||||
f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd {app_dir} && exec bash -il"'""",
|
|
||||||
shell=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def btop(ctx: Context) -> None:
|
def btop(ctx: Context) -> None:
|
||||||
"""Запустить btop на удалённом сервере"""
|
"""Запустить btop на удалённом сервере"""
|
||||||
subprocess.run(f"ssh {_remote_user()}@{_remote_host()} -t btop", shell=True)
|
ctx.run(f"ssh {_remote_user()}@{_remote_host()} -t btop", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
@@ -130,6 +109,46 @@ def decrypt(ctx: Context, file: str) -> None:
|
|||||||
ctx.run(f"uv run ansible-vault decrypt {file}")
|
ctx.run(f"uv run ansible-vault decrypt {file}")
|
||||||
|
|
||||||
|
|
||||||
|
@task(aliases=["ee"])
|
||||||
|
def edit_encrypted(ctx: Context, path: str) -> None:
|
||||||
|
"""Редактировать vault-файл через ansible-vault edit (без расшифровки на диск)"""
|
||||||
|
ctx.run(f"uv run ansible-vault edit {path}", pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Специфичные для этого репозитория задачи ---
|
||||||
|
|
||||||
|
|
||||||
|
def _application_dir() -> str:
|
||||||
|
"""Чтение application_dir: сначала из inventory (override), затем из vars/vars.yml."""
|
||||||
|
inv_value = _yq('.ungrouped.hosts.server.application_dir // ""')
|
||||||
|
if inv_value:
|
||||||
|
return inv_value
|
||||||
|
result = subprocess.run(
|
||||||
|
["yq", ".application_dir", VARS_FILE],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
value = result.stdout.strip()
|
||||||
|
if not value or value == "null":
|
||||||
|
raise Exit(
|
||||||
|
f"application_dir не определён ни в inventory, ни в {VARS_FILE}", code=1
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@task(aliases=["login"])
|
||||||
|
def login_as_app(ctx: Context, app: str) -> None:
|
||||||
|
"""SSH и переключиться на пользователя приложения: inv login gitea"""
|
||||||
|
# sudo -i: login shell, -u: от имени пользователя
|
||||||
|
# bash -i: интерактивный режим (job control), -l: login (читает профиль)
|
||||||
|
app_dir = f"{_application_dir()}/{app}"
|
||||||
|
subprocess.run(
|
||||||
|
f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd {app_dir} && exec bash -il"'""",
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def authelia_cli(ctx: Context, args: str = "") -> None:
|
def authelia_cli(ctx: Context, args: str = "") -> None:
|
||||||
"""Запустить authelia CLI в docker"""
|
"""Запустить authelia CLI в docker"""
|
||||||
|
|||||||
Reference in New Issue
Block a user