Compare commits

..
12 Commits
Author SHA1 Message Date
av c5217607b1 owner: группа docker убрана у сервисных пользователей
Linting / YAML Lint (push) Canceled after 0s
Linting / Ansible Lint (push) Canceled after 0s
- 20 приложений её не получают: контейнерами рулит ansible от root, а
  членство в группе docker равносильно root на хосте
- у gitea, miniflux и outline группа осталась — их backup.sh дампит базу
  через docker compose exec из-под самого приложения
- задача service-users-docker-group закрыта, остаток вынесен в новую
  backup-dump-without-docker-group
2026-08-22 19:40:57 +03:00
av 856bf72560 owner: роль причёсана под конвенции eget и secrets
- валидация аргументов через assert вместо пары fail с одинаковым именем
- with_items/with_dict заменены на loop, имена задач без точки на конце
- добавлены meta/main.yml и README с таблицей переменных
- задача ansible-owner-role-cleanup удалена из беклога
2026-08-22 19:27:33 +03:00
av 70372ee028 caddyproxy: HSTS для всех сайтов и мелкий хардненинг
- сниппет access_log переименован в common, туда добавлен заголовок
  Strict-Transport-Security на год; без includeSubDomains — под
  vakhrushev.me есть имена не с этого сервера
- у edge-контейнера убрана capability NET_ADMIN, reverse-proxy её не требует
- убран leftover-листенер :29999 у status.vakhrushev.me
- задача caddy-hardening-nits удалена из беклога
2026-08-22 19:14:34 +03:00
av 16f0712a31 caddyproxy: Caddyfile применяется через caddy reload
- вместо рестарта всего прокси, через который идёт весь трафик; конфиг
  указан явно через --config, иначе caddy молча перезагружает не тот файл
- задача handlers-remaining-apps удалена из беклога
2026-08-22 19:08:08 +03:00
av 3bb85b85f6 handlers рестарта для apprise, dashboard, transcriber, miniflux
- правка bind-моунтнутого конфига раньше не подхватывалась до пересоздания
  контейнера; теперь template/secrets шлют notify на рестарт
- miniflux: убран recreate: always, вместо него точечный рестарт miniflux_app
  по изменению файлов секретов, postgres больше не дёргается
- goaccess: рестарт не нужен, build: always пересоздаёт контейнер по новому
  id образа — зафиксировано комментарием
2026-08-22 19:08:05 +03:00
av 7e64d76d02 ufw: правило для zabbix-агента и включение в all-setup
Linting / YAML Lint (push) Canceled after 0s
Linting / Ansible Lint (push) Canceled after 0s
- Порт 10050 (агент Timeweb) открыт только опросчикам хостера из Server= его
  конфига, а не всему интернету.
- playbook-ufw.yml добавлен в playbook-all-setup.yml перед docker: раньше он
  запускался только руками и после переезда на Timeweb не был прогнан вовсе —
  на сервере ufw до сих пор с ENABLED=no и пустым набором правил.
- В шапке плейбука зафиксировано, что ufw не закрывает порты, опубликованные
  докером: они идут мимо цепочки INPUT.
2026-08-22 18:00:24 +03:00
av 6bb0d61183 caddyproxy: admin API закрыт на loopback, метрики вынесены на :2020
- `admin :2019` слушал всю web_proxy_network: любой контейнер мог POST-ом в
  /config/ переписать маршрутизацию и снять forward_auth. Теперь
  `admin localhost:2019`.
- Метрики для netdata отдаёт отдельный site-блок `http://:2020 { metrics
  /metrics }`, netdata скрейпит новый порт.
- Порт для метрик именно 2020, а не 2019, как предлагала задача: сайт на :2019
  перехватывает IPv6-петлю (admin висит только на IPv4), из-за чего
  `caddy reload` внутри контейнера возвращает 0 и молча ничего не применяет —
  проверено на caddy:2.11.4.
2026-08-22 17:44:08 +03:00
av 304263e7b1 authelia: уровень логов понижен с debug до info
- debug-логи содержат детали аутентификации, имена пользователей и внутренности
  OIDC-потоков — в обычной эксплуатации не нужны, а через docker logs/dozzle это
  лишняя поверхность утечки.
- Задача убрана из беклога.
2026-08-22 17:38:56 +03:00
av 25886d1be0 miniflux: /metrics закрыт снаружи в Caddy
- METRICS_ALLOWED_NETWORKS не помогает: за прокси Miniflux видит IP Caddy,
  а netdata скрейпит по той же web_proxy_network — по IP их не различить
- задача убрана из беклога
2026-08-22 17:35:00 +03:00
av b9be642c5c gitea: ветка по умолчанию в новых репозиториях — master 2026-08-22 17:29:22 +03:00
av 9c75a52529 Dashboard: update to 2.1.2
Мажорное обновление homepage. Единственное ломающее изменение 2.0.0 —
встроенная аутентификация, она включается только через HOMEPAGE_AUTH_ENABLED,
так что дашборд остаётся за Authelia как прежде.
2026-08-22 13:39:01 +03:00
av a909442d8f образы приложений прописаны в docker-compose явно
- dashboard и wanderer брали имя образа и версию из переменных плейбука;
  теперь, как у остальных сервисов, они видны прямо в compose
- переменные homepage_image и wanderer_version удалены за ненадобностью
2026-08-22 13:38:36 +03:00
45 changed files with 324 additions and 239 deletions
+1 -7
View File
@@ -17,24 +17,20 @@ _(sec-ревью)_ — три независимых security-ревью на м
## Высокий ## Высокий
- [Закрыть admin API Caddy (localhost вместо всей сети)](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-ревью)_ - [Изолировать/запаролить сессионный redis Authelia](authelia-redis-isolation.md) — стор сессий SSO в общей сети без пароля; запись в него = hijack SSO всех сервисов _(sec-ревью)_
- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata - [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata
- [Никаких секретов в выводе плейбуков](no-secrets-in-playbook-output.md) — `inv pl` всегда с `--diff`, и каждая задача, рендерящая файл с секретом, печатает его в терминал; `no_log` стоит ровно в одном месте из четырнадцати - [Никаких секретов в выводе плейбуков](no-secrets-in-playbook-output.md) — `inv pl` всегда с `--diff`, и каждая задача, рендерящая файл с секретом, печатает его в терминал; `no_log` стоит ровно в одном месте из четырнадцати
## Средний ## Средний
- [Дампы бэкапов без группы `docker`](backup-dump-without-docker-group.md) — у miniflux/gitea/outline группа осталась: их backup.sh дампит через `docker compose exec` из-под самого приложения
- [Захардить мониторинг-стек (netdata / dozzle / goaccess)](harden-monitoring-stack.md) — netdata/dozzle: лишние привилегии + docker.sock + доверие Remote-User в общей сети; socket-proxy + monitoring_network _(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-ревью)_ - [Закрыть calibre-web forward-auth и проверить дефолтные креды](calibre-forward-auth.md) — домен без forward_auth, защита на встроенной авторизации с дефолтом `admin/admin123` _(sec-ревью)_
- [Понизить log level Authelia с debug до info](authelia-log-level.md) — `debug` в проде утекает детали аутентификации и OIDC через `docker logs`/dozzle _(sec-ревью)_
- [Включить автообновления безопасности](security-autoupdates.md) — патчи openssh/libssl только вручную; окна в недели между CVE и фиксом _(sec-ревью)_ - [Включить автообновления безопасности](security-autoupdates.md) — патчи openssh/libssl только вручную; окна в недели между CVE и фиксом _(sec-ревью)_
- [Запретить SSH-вход под root](ssh-disable-root-login.md) — `PermitRootLogin yes` пускает root по ключу; наименьшие привилегии + defense-in-depth (пароль уже off) _(sec-ревью)_ - [Запретить SSH-вход под root](ssh-disable-root-login.md) — `PermitRootLogin yes` пускает root по ключу; наименьшие привилегии + defense-in-depth (пароль уже off) _(sec-ревью)_
- [Gitea runner on-demand в Yandex Cloud](gitea-runner-on-demand.md) — раннер активен только во время сборки; webhook→Cloud Function стартует ВМ, probe/decide гасят по idle; экономия ~95% - [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 - [Синхронизация общих Ansible-ролей между репозиториями](shared-roles-sync.md) — `owner`/`eget`/`secrets` дублируются между репозиториями и дрейфуют; канон в ansible-shared + rsync-таски invoke
- [Уведомления о размере приложений и заполнении диска](disk-usage-alerts.md) — алерты на заполнение диска и аномальный рост data-томов; сервер ресурсно ограничен _(tududi)_ - [Уведомления о размере приложений и заполнении диска](disk-usage-alerts.md) — алерты на заполнение диска и аномальный рост data-томов; сервер ресурсно ограничен _(tududi)_
- [Handlers рестарта для оставшихся приложений](handlers-remaining-apps.md) — apprise/dashboard/transcriber/miniflux монтируют конфиг в контейнер, но рестарта нет вовсе; правка конфига не подхватывается
- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_ - [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_
- [Вывести из оборота секреты с широкими правами](narrow-secret-scopes.md) — `yc_oauth_token` (весь аккаунт Yandex Cloud) уже выведен; осталось проставить радиус остальным переменным vault и сузить широкие - [Вывести из оборота секреты с широкими правами](narrow-secret-scopes.md) — `yc_oauth_token` (весь аккаунт Yandex Cloud) уже выведен; осталось проставить радиус остальным переменным vault и сузить широкие
- [outline: секреты из файлов](secrets-file-outline.md) — умеет generic `<NAME>_FILE` для всех пяти секретов; единственное приложение, где выносится вообще всё, включая пароль БД - [outline: секреты из файлов](secrets-file-outline.md) — умеет generic `<NAME>_FILE` для всех пяти секретов; единственное приложение, где выносится вообще всё, включая пароль БД
@@ -44,10 +40,8 @@ _(sec-ревью)_ — три независимых security-ревью на м
## Низкий ## Низкий
- [Мелкий хардненинг 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-ревью)_ - [Мелкий хардненинг контейнеров](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-ревью)_ - [Унифицировать ACL техпанелей Authelia](authelia-acl-unify.md) — rssbridge (SSRF) и dashboard за `one_factor` без subject; свести к `group:admins` + `two_factor` _(sec-ревью)_
- [Причесать роль `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 _(ансибл-ревью)_ - [Инвентарь: `host_vars`, именованные группы, точечный `become`](ansible-inventory-hostvars.md) — хост-специфику в `host_vars/server.yml`, хост в именованную группу, глобальный root → точечный become _(ансибл-ревью)_
- [Фоновая зачистка стиля и конфигурации Ansible](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_ - [Фоновая зачистка стиля и конфигурации Ansible](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_
- [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_ - [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_
@@ -1,20 +0,0 @@
# Причесать роль `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.
-13
View File
@@ -1,13 +0,0 @@
# Понизить 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,33 @@
# Дампы бэкапов без группы `docker` у сервисных пользователей
**Приоритет:** средний
Группу `docker` (= root на хосте) убрали у всех сервисных пользователей, кроме
трёх: `miniflux`, `gitea`, `outline`. Их `backup.sh` делает `docker compose exec`
(pg_dump, `gitea dump`), а `files/backups/backup-all.py` запускает такие скрипты
через `su --login <app_user>`, то есть от имени самого приложения — без членства
в группе дамп упадёт.
Дампить снаружи докера нельзя: postgres порт не публикует, `gitea dump` живёт
только внутри контейнера.
Варианты, как развязать:
- **A. sudo-обёртка на дамп.** Плейбук кладёт `/usr/local/sbin/backup-<app>-dump.sh`
(root:root, 0755 — сам app_user его не перепишет) с единственным
`docker compose exec …`, в sudoers — `<app_user> ALL=(root) NOPASSWD:` ровно на
этот файл, а `backup.sh` дёргает его через `sudo`. Пользователь получает право
на одну фиксированную команду вместо права на весь докер. Механизм в проекте
уже обкатан: так `playbook-backups.yml` разрешает `primary_user` запускать
`backup-all.py`.
- **B. root-скрипты в оркестраторе.** Новый контракт в `backup-all.py`: если у
приложения есть `backup-root.sh` с владельцем root — запускать его без `su`.
Механизм один на всех, но от root пойдёт скрипт целиком, включая
`keep-files.py`, и дампы станут root-овыми файлами в директориях приложения.
- **C. оставить как есть** — три аккаунта как осознанное исключение,
задокументированное в плейбуках.
Предпочтение на момент записи — A.
Связано: playbook-miniflux.yml, playbook-gitea.yml, playbook-outline.yml,
files/*/backup.template.sh, files/backups/backup-all.py.
-16
View File
@@ -1,16 +0,0 @@
# Закрыть 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
@@ -1,19 +0,0 @@
# Мелкий хардненинг 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.
-30
View File
@@ -1,30 +0,0 @@
# Handlers рестарта для оставшихся приложений
**Приоритет:** средний
Паттерн `handlers` + `notify` внедрён в caddyproxy, netdata, authelia, bifrost,
remembos (см. задачу `ansible-handlers-restart`, закрыта). Остались плейбуки, где
конфиг bind-моунтится в контейнер, но рестарта нет **вообще** — правка конфига без
изменения docker-compose.yml не подхватывается до следующего пересоздания
контейнера:
- `playbook-apprise.yml``backups.cfg``config_dir:/config`.
- `playbook-dashboard.yml``config/*.yaml``config_dir:/app/config`.
- `playbook-transcriber.yml``config.toml` bind-моунтится **отдельным файлом**
(`{{ config_file }}:/config/config.toml:ro`). Худший случай: `template`
перезаписывает файл с новым inode, и контейнер продолжает видеть старый до
рестарта.
- `playbook-miniflux.yml` — файлы секретов роли `secrets``secrets_dir:/secrets:ro`,
читаются через `*_FILE` при старте.
Заодно проверить `playbook-goaccess.yml`: конфиг там не монтируется, но
`Dockerfile`/`entrypoint.sh` меняются без изменения compose-файла — надо
убедиться, что `build: always` действительно пересоздаёт контейнер.
Отдельный кандидат: caddy сейчас перезапускается целиком, хотя умеет
`caddy reload` — применение Caddyfile без разрыва соединений на реверс-прокси,
через который идёт весь трафик. В `playbook-caddyproxy.yml` для этого уже лежит
закомментированный блок с `docker_compose_v2_exec`.
Связано: playbook-apprise.yml, playbook-dashboard.yml, playbook-transcriber.yml,
playbook-miniflux.yml, playbook-goaccess.yml, playbook-caddyproxy.yml.
-16
View File
@@ -1,16 +0,0 @@
# Ограничить эндпоинт `/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.
@@ -1,20 +0,0 @@
# Убрать группу `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-file-*`
(секреты в `environment:` контейнеров): членство в группе `docker` — как раз тот
доступ, которым такие переменные и вычитываются.
Связано: все playbook-*.yml (owner_extra_groups), roles/owner, playbook-docker.yml.
+1 -1
View File
@@ -121,7 +121,7 @@ server:
## ##
log: log:
## Level of verbosity for logs: info, debug, trace. ## Level of verbosity for logs: info, debug, trace.
level: "debug" level: "info"
## Format the logs are written as: json, text. ## Format the logs are written as: json, text.
format: "json" format: "json"
+61 -29
View File
@@ -5,21 +5,38 @@
grace_period 15s grace_period 15s
# Admin API слушает только loopback контейнера: на :2019 его видела бы вся # Admin API слушает только loopback контейнера: на :2019 его видела бы вся
# web_proxy_network, и любое приложение могло бы переписать маршрутизацию
# или снять forward_auth. Метрики вынесены в отдельный site-блок ниже, и
# порт у него другой: сайт на :2019 перехватывал бы IPv6-петлю (admin висит
# только на IPv4), и `caddy reload` молча ничего не применял бы.
admin localhost:2019
# Enable metrics in Prometheus format # Enable metrics in Prometheus format
# https://caddyserver.com/docs/metrics # https://caddyserver.com/docs/metrics
metrics metrics
} }
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Metrics
# -------------------------------------------------------------------
# Prometheus-метрики для netdata (files/netdata/go.d/prometheus.conf).
# Наружу порт не публикуется — доступен только внутри web_proxy_network.
http://:2020 {
metrics /metrics
}
# -------------------------------------------------------------------
# Snippets # Snippets
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Общие настройки всех сайтов: access-лог и HSTS. # Общие настройки всех сайтов: access-лог и HSTS.
# Mode 644 lets read-only consumers (goaccess and ad-hoc host-side tail) #
# read the file; lumberjack would otherwise default to 0600. # Лог общий для всех сайтов, его разбирает GoAccess. Mode 644 нужен, чтобы
(access_log) { # файл читали потребители на хосте (goaccess и ad-hoc tail): lumberjack иначе
log { # ставит 0600.
(common) {
log {
output file /var/log/caddy/access.log { output file /var/log/caddy/access.log {
mode 644 mode 644
roll_size 100mib roll_size 100mib
@@ -29,6 +46,12 @@
format json format json
} }
# HSTS: браузер, однажды увидевший заголовок, целый год ходит на это имя только
# по https, минуя редирект с http, — закрывает downgrade и SSL-strip.
# Без includeSubDomains: политика по каждому имени своя, а под vakhrushev.me
# есть поддомены не с этого сервера (например photo.vakhrushev.me).
header Strict-Transport-Security "max-age=31536000"
}
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Applications # Applications
@@ -37,7 +60,7 @@
vakhrushev.me { vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
# Matrix federation delegation: tells other servers/clients that the # Matrix federation delegation: tells other servers/clients that the
# homeserver for vakhrushev.me lives at matrix.vakhrushev.me. # homeserver for vakhrushev.me lives at matrix.vakhrushev.me.
# https://spec.matrix.org/latest/server-server-api/#server-discovery # https://spec.matrix.org/latest/server-server-api/#server-discovery
@@ -65,7 +88,7 @@ vakhrushev.me {
start.vakhrushev.me { start.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -79,7 +102,7 @@ start.vakhrushev.me {
matrix.vakhrushev.me { matrix.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to tuwunel_app:6167 to tuwunel_app:6167
} }
@@ -88,14 +111,14 @@ matrix.vakhrushev.me {
auth.vakhrushev.me { auth.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy authelia_app:9091 reverse_proxy authelia_app:9091
} }
status.vakhrushev.me { status.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -107,7 +130,7 @@ status.vakhrushev.me, :29999 {
git.vakhrushev.me { git.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to gitea_app:3000 to gitea_app:3000
} }
@@ -116,7 +139,7 @@ git.vakhrushev.me {
outline.vakhrushev.me { outline.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to outline_app:3000 to outline_app:3000
} }
@@ -125,7 +148,7 @@ outline.vakhrushev.me {
gramps.vakhrushev.me { gramps.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to gramps_app:5000 to gramps_app:5000
} }
@@ -134,16 +157,25 @@ gramps.vakhrushev.me {
miniflux.vakhrushev.me { miniflux.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
# /metrics наружу не отдаём. METRICS_ALLOWED_NETWORKS тут не помогает: за # /metrics наружу не отдаём. METRICS_ALLOWED_NETWORKS тут не помогает: за
to miniflux_app:8080 # прокси Miniflux видит приватный IP контейнера Caddy, а не адрес клиента.
} # Netdata скрейпит miniflux_app:8080/metrics напрямую по докер-сети, мимо Caddy.
handle /metrics {
respond 404
}
handle {
reverse_proxy {
to miniflux_app:8080
}
}
} }
wakapi.vakhrushev.me { wakapi.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to wakapi_app:3000 to wakapi_app:3000
} }
@@ -152,7 +184,7 @@ wakapi.vakhrushev.me {
wanderer.vakhrushev.me { wanderer.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to wanderer_web:3000 to wanderer_web:3000
} }
@@ -161,7 +193,7 @@ wanderer.vakhrushev.me {
memos.vakhrushev.me { memos.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to memos_app:5230 to memos_app:5230
} }
@@ -172,7 +204,7 @@ memos.vakhrushev.me {
tududi.vakhrushev.me { tududi.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to tududi_app:3002 to tududi_app:3002
} }
@@ -181,7 +213,7 @@ tududi.vakhrushev.me {
remembos.vakhrushev.me { remembos.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -195,7 +227,7 @@ remembos.vakhrushev.me {
calibre.vakhrushev.me { calibre.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to calibre_web_app:8083 to calibre_web_app:8083
} }
@@ -204,7 +236,7 @@ calibre.vakhrushev.me {
wanderbase.vakhrushev.me { wanderbase.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -218,7 +250,7 @@ wanderbase.vakhrushev.me {
rssbridge.vakhrushev.me { rssbridge.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -232,7 +264,7 @@ rssbridge.vakhrushev.me {
dozzle.vakhrushev.me { dozzle.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter
@@ -252,7 +284,7 @@ dozzle.vakhrushev.me {
{{ goatcounter_sites | join(', ') }} { {{ goatcounter_sites | join(', ') }} {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
reverse_proxy { reverse_proxy {
to goatcounter_app:8080 to goatcounter_app:8080
} }
@@ -261,7 +293,7 @@ dozzle.vakhrushev.me {
goaccess.vakhrushev.me { goaccess.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -281,7 +313,7 @@ goaccess.vakhrushev.me {
bifrost.vakhrushev.me { bifrost.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import common import common
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
@@ -7,8 +7,6 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "443:443/udp" - "443:443/udp"
cap_add:
- NET_ADMIN
volumes: volumes:
- "{{ caddy_file_dir }}:/etc/caddy" - "{{ caddy_file_dir }}:/etc/caddy"
- "{{ data_dir }}:/data" - "{{ data_dir }}:/data"
+3 -2
View File
@@ -7,7 +7,8 @@
services: services:
dashboard_app: dashboard_app:
image: "{{ homepage_image }}" # See versions: https://github.com/gethomepage/homepage/releases
image: ghcr.io/gethomepage/homepage:v2.1.2
container_name: dashboard_app container_name: dashboard_app
restart: unless-stopped restart: unless-stopped
user: "{{ app_owner_uid }}:{{ app_owner_gid }}" user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
@@ -18,7 +19,7 @@ services:
- "/etc/localtime:/etc/localtime:ro" - "/etc/localtime:/etc/localtime:ro"
- "/etc/timezone:/etc/timezone:ro" - "/etc/timezone:/etc/timezone:ro"
environment: environment:
# homepage 1.x требует явный whitelist Host за реверс-прокси, иначе отдаёт # homepage требует явный whitelist Host за реверс-прокси, иначе отдаёт
# «Host validation failed». Пускаем только адрес из Caddy. # «Host validation failed». Пускаем только адрес из Caddy.
- HOMEPAGE_ALLOWED_HOSTS={{ homepage_domain }} - HOMEPAGE_ALLOWED_HOSTS={{ homepage_domain }}
+3
View File
@@ -17,6 +17,9 @@ services:
- "USER_GID={{ owner_create_result.group }}" - "USER_GID={{ owner_create_result.group }}"
- "GITEA__server__SSH_PORT=2222" - "GITEA__server__SSH_PORT=2222"
# Ветка по умолчанию в новых репозиториях (у Gitea по умолчанию main)
- "GITEA__repository__DEFAULT_BRANCH=master"
# Mailer # Mailer
- "GITEA__mailer__ENABLED=true" - "GITEA__mailer__ENABLED=true"
- "GITEA__mailer__PROTOCOL=smtp+starttls" - "GITEA__mailer__PROTOCOL=smtp+starttls"
@@ -33,6 +33,9 @@ services:
- OAUTH2_PROVIDER=oidc - OAUTH2_PROVIDER=oidc
- OAUTH2_REDIRECT_URL=https://miniflux.vakhrushev.me/oauth2/oidc/callback - OAUTH2_REDIRECT_URL=https://miniflux.vakhrushev.me/oauth2/oidc/callback
- OAUTH2_USER_CREATION=1 - OAUTH2_USER_CREATION=1
# Метрики скрейпит netdata по докер-сети (miniflux_app:8080/metrics).
# Сузить METRICS_ALLOWED_NETWORKS нельзя: netdata и Caddy сидят в одной
# web_proxy_network, по IP их не различить. Наружу /metrics закрыт в Caddy.
- METRICS_COLLECTOR=1 - METRICS_COLLECTOR=1
- METRICS_ALLOWED_NETWORKS=0.0.0.0/0 - METRICS_ALLOWED_NETWORKS=0.0.0.0/0
+1 -1
View File
@@ -3,7 +3,7 @@ update_every: 15
jobs: jobs:
- name: caddyproxy - name: caddyproxy
url: http://caddyproxy:2019/metrics url: http://caddyproxy:2020/metrics
selector: selector:
allow: allow:
- "caddy_http_*" - "caddy_http_*"
+2 -2
View File
@@ -27,7 +27,7 @@ services:
wanderer_db: wanderer_db:
container_name: wanderer_db container_name: wanderer_db
image: "flomp/wanderer-db:{{ wanderer_version }}" image: flomp/wanderer-db:v0.18.3
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on: depends_on:
wanderer_search: wanderer_search:
@@ -53,7 +53,7 @@ services:
wanderer_web: wanderer_web:
container_name: wanderer_web container_name: wanderer_web
image: "flomp/wanderer-web:{{ wanderer_version }}" image: flomp/wanderer-web:v0.18.3
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on: depends_on:
wanderer_search: wanderer_search:
+3
View File
@@ -2,6 +2,9 @@
- name: 'Configure system' - name: 'Configure system'
ansible.builtin.import_playbook: playbook-system.yml ansible.builtin.import_playbook: playbook-system.yml
- name: 'Configure firewall'
ansible.builtin.import_playbook: playbook-ufw.yml
- name: 'Configure docker' - name: 'Configure docker'
ansible.builtin.import_playbook: playbook-docker.yml ansible.builtin.import_playbook: playbook-docker.yml
+12 -1
View File
@@ -27,7 +27,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
@@ -56,6 +55,7 @@
mode: "0640" mode: "0640"
# Файл содержит секреты из vault — без no_log их печатает --diff. # Файл содержит секреты из vault — без no_log их печатает --diff.
no_log: true no_log: true
notify: "Restart application"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
@@ -72,3 +72,14 @@
remove_orphans: true remove_orphans: true
tags: tags:
- run-app - run-app
handlers:
# Приложение читает конфиги только при старте, а `state: present` не
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
# правку конфига применяет только явный рестарт.
- name: "Restart application"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "restarted"
tags:
- run-app
-1
View File
@@ -24,7 +24,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -21,7 +21,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
+10 -16
View File
@@ -23,7 +23,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
@@ -74,7 +73,7 @@
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0640" mode: "0640"
notify: "Restart application" notify: "Reload caddy configuration"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
@@ -92,21 +91,16 @@
tags: tags:
- run-app - run-app
# - name: "Reload caddy"
# community.docker.docker_compose_v2_exec:
# project_src: '{{ base_dir }}'
# service: "{{ service_name }}"
# command: caddy reload --config /etc/caddy/Caddyfile
# tags:
# - run-app
handlers: handlers:
# Приложение читает конфиги только при старте, а `state: present` не # `state: present` не пересоздаёт контейнер, если docker-compose.yml не
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому # изменился, — правку Caddyfile надо применять отдельно. Через прокси идёт
# правку конфига применяет только явный рестарт. # весь трафик, поэтому не рестарт, а `caddy reload`: конфиг применяется без
- name: "Restart application" # разрыва соединений. Конфиг указываем явно — без --config caddy берёт файл
community.docker.docker_compose_v2: # из своего рабочего каталога и молча перезагружает не то.
- name: "Reload caddy configuration"
community.docker.docker_compose_v2_exec:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "restarted" service: "{{ service_name }}"
command: "caddy reload --config /etc/caddy/Caddyfile"
tags: tags:
- run-app - run-app
-1
View File
@@ -19,7 +19,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
+13 -4
View File
@@ -10,11 +10,9 @@
base_dir: "{{ (application_dir, app_name) | path_join }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}" config_dir: "{{ (base_dir, 'config') | path_join }}"
# gethomepage.dev — проверь актуальный тег перед обновлением.
homepage_image: "ghcr.io/gethomepage/homepage:v1.13.2"
# Внутренний порт контейнера; наружу не публикуется, ходим только через Caddy. # Внутренний порт контейнера; наружу не публикуется, ходим только через Caddy.
homepage_port: 3000 homepage_port: 3000
# Домен, на котором Caddy отдаёт дашборд. homepage 1.x требует явный whitelist # Домен, на котором Caddy отдаёт дашборд. homepage требует явный whitelist
# Host за реверс-прокси, иначе «Host validation failed». # Host за реверс-прокси, иначе «Host validation failed».
homepage_domain: "start.vakhrushev.me" homepage_domain: "start.vakhrushev.me"
@@ -32,7 +30,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
@@ -53,6 +50,7 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0640" mode: "0640"
loop: "{{ config_files }}" loop: "{{ config_files }}"
notify: "Restart application"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
@@ -70,3 +68,14 @@
pull: "always" pull: "always"
tags: tags:
- run-app - run-app
handlers:
# Приложение читает конфиги только при старте, а `state: present` не
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
# правку конфига применяет только явный рестарт.
- name: "Restart application"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "restarted"
tags:
- run-app
-1
View File
@@ -17,7 +17,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
+4
View File
@@ -19,6 +19,10 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
# приложений, чей backup.sh делает docker compose exec: оркестратор
# запускает такие скрипты через su от имени этого пользователя.
# См. docs/backlog/backup-dump-without-docker-group.md.
owner_extra_groups: ["docker"] owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
+3 -1
View File
@@ -20,7 +20,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
@@ -83,6 +82,9 @@
- { name: "Dockerfile", mode: "0640" } - { name: "Dockerfile", mode: "0640" }
- { name: "entrypoint.sh", mode: "0750" } - { name: "entrypoint.sh", mode: "0750" }
# Handler рестарта здесь не нужен: конфиг в контейнер не монтируется, а
# правку Dockerfile/entrypoint.sh применяет `build: always` — пересобранный
# образ получает новый id, и compose пересоздаёт по нему контейнер (проверено).
- name: "Run application with docker compose" - name: "Run application with docker compose"
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
-1
View File
@@ -29,7 +29,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -22,7 +22,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -23,7 +23,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -20,7 +20,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
+23 -1
View File
@@ -21,6 +21,10 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
# приложений, чей backup.sh делает docker compose exec: оркестратор
# запускает такие скрипты через su от имени этого пользователя.
# См. docs/backlog/backup-dump-without-docker-group.md.
owner_extra_groups: ["docker"] owner_extra_groups: ["docker"]
- name: "Create internal directories" - name: "Create internal directories"
@@ -37,9 +41,13 @@
- "{{ postgres_data_dir }}" - "{{ postgres_data_dir }}"
- "{{ postgres_backups_dir }}" - "{{ postgres_backups_dir }}"
# notify на import_role наследуется всеми задачами роли: рестарт нужен,
# если изменился любой файл секретов — miniflux читает их через *_FILE
# только при старте.
- name: "Copy secrets" - name: "Copy secrets"
ansible.builtin.import_role: ansible.builtin.import_role:
name: secrets name: secrets
notify: "Restart application"
vars: vars:
secrets_dest: "{{ secrets_dir }}" secrets_dest: "{{ secrets_dir }}"
secrets_user: "{{ app_user }}" secrets_user: "{{ app_user }}"
@@ -72,7 +80,21 @@
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
recreate: "always"
remove_orphans: true remove_orphans: true
tags: tags:
- run-app - run-app
handlers:
# Приложение читает секреты только при старте, а `state: present` не
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
# правку применяет только явный рестарт.
- name: "Restart application"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
# Только сам miniflux: базу дёргать незачем, пароль postgres читается
# из тех же секретов лишь при инициализации кластера.
services:
- "miniflux_app"
state: "restarted"
tags:
- run-app
-1
View File
@@ -21,7 +21,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
+4
View File
@@ -25,6 +25,10 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
# приложений, чей backup.sh делает docker compose exec: оркестратор
# запускает такие скрипты через su от имени этого пользователя.
# См. docs/backlog/backup-dump-without-docker-group.md.
owner_extra_groups: ["docker"] owner_extra_groups: ["docker"]
- name: "Create internal directories" - name: "Create internal directories"
-1
View File
@@ -30,7 +30,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -17,7 +17,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories" - name: "Create internal application directories"
ansible.builtin.file: ansible.builtin.file:
+13 -1
View File
@@ -23,7 +23,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
@@ -48,6 +47,7 @@
# Vault-файл: ansible расшифровывает его при копировании, и без no_log # Vault-файл: ansible расшифровывает его при копировании, и без no_log
# --diff покажет содержимое открытым текстом. # --diff покажет содержимое открытым текстом.
no_log: true no_log: true
notify: "Restart application"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
@@ -64,3 +64,15 @@
remove_orphans: true remove_orphans: true
tags: tags:
- run-app - run-app
handlers:
# Конфиг монтируется отдельным файлом, а `copy` подменяет его новым inode:
# запущенный контейнер продолжает видеть старый файл по старому inode.
# Bind-моунт переустанавливается при старте контейнера, поэтому применяет
# правку только явный рестарт.
- name: "Restart application"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "restarted"
tags:
- run-app
-1
View File
@@ -20,7 +20,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-1
View File
@@ -23,7 +23,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
+23
View File
@@ -1,7 +1,19 @@
--- ---
# ufw фильтрует только цепочку INPUT, то есть хостовые сервисы (sshd,
# zabbix-агент). Порты, опубликованные докером (80, 443, 2222), идут через
# PREROUTING/FORWARD мимо ufw — закрыть их можно только правкой `ports:` в
# docker-compose соответствующего приложения.
- name: "Configure UFW firewall" - name: "Configure UFW firewall"
hosts: all hosts: all
vars:
# Опросчики Zabbix у Timeweb: только им нужен доступ к агенту на хосте.
# Список — из Server= в /etc/zabbix/zabbix_agentd.conf.
zabbix_poller_ips:
- "92.53.116.12"
- "92.53.116.111"
- "92.53.116.119"
tasks: tasks:
- name: "Ensure UFW is installed" - name: "Ensure UFW is installed"
ansible.builtin.apt: ansible.builtin.apt:
@@ -49,6 +61,17 @@
port: "443" port: "443"
proto: udp proto: udp
# Агент хостера слушает 0.0.0.0:10050 и сам проверяет адрес источника,
# но держать порт открытым для всего интернета незачем.
- name: "Allow Zabbix agent on port 10050 from Timeweb pollers"
community.general.ufw:
rule: allow
src: "{{ item }}"
port: "10050"
proto: tcp
comment: "Timeweb zabbix poller"
loop: "{{ zabbix_poller_ips }}"
- name: "Enable UFW" - name: "Enable UFW"
community.general.ufw: community.general.ufw:
state: enabled state: enabled
-1
View File
@@ -20,7 +20,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
-2
View File
@@ -12,7 +12,6 @@
backups_dir: "{{ (base_dir, 'backups') | path_join }}" backups_dir: "{{ (base_dir, 'backups') | path_join }}"
gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}" gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}"
wanderer_version: "v0.18.3"
wanderer_origin: "https://wanderer.vakhrushev.me" wanderer_origin: "https://wanderer.vakhrushev.me"
tasks: tasks:
@@ -23,7 +22,6 @@
owner_name: "{{ app_user }}" owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}" owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}" owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories" - name: "Create application internal directories"
ansible.builtin.file: ansible.builtin.file:
+59
View File
@@ -0,0 +1,59 @@
# owner
Заводит системного пользователя и группу под одно приложение: фиксированные
uid/gid, ssh-ключи и переменные окружения в `~/.env`.
Один сервис — один пользователь: контейнеры приложения работают от его uid/gid,
и файлы в `base_dir` принадлежат ему же. Роль вызывается первой задачей почти
каждого `playbook-<app>.yml`.
## Что делает
1. **Группа и пользователь.** Создаёт группу `owner_group` с `owner_gid` и
пользователя `owner_name` с `owner_uid` и шеллом `/bin/bash`. Номера задаёт
плейбук приложения (соглашение по нумерации — в `AGENTS.md`).
2. **SSH-ключи.** Прописывает `owner_ssh_keys` в `authorized_keys`.
3. **Переменные окружения.** Рендерит `~/.env` из `owner_env` плюс `USER_UID` и
`USER_GID` — их подставляют docker-compose шаблоны приложений. В `.bashrc`
добавляет строку, подгружающую `~/.env` целиком, и вычищает старые построчные
`export`-ы, оставшиеся от прежней схемы.
## Результат для плейбука
Роль регистрирует `owner_create_result` — результат модуля `user`. Плейбуки и
compose-шаблоны берут оттуда `owner_create_result.uid` и
`owner_create_result.group` (это gid) для `user:` в docker-compose.
## Переменные
Полный список и значения по умолчанию — в `defaults/main.yml`.
| Переменная | Обязательная | Описание |
| --------------------- | ------------ | --------------------------------------------------- |
| `owner_name` | да | Имя пользователя; по умолчанию оно же имя группы |
| `owner_uid` | да | Uid пользователя |
| `owner_group` | нет | Имя группы, по умолчанию `owner_name` |
| `owner_gid` | нет | Gid группы, по умолчанию `owner_uid` |
| `owner_extra_groups` | нет | Дополнительные группы; по умолчанию пусто |
| `owner_ssh_keys` | нет | Список публичных ключей для `authorized_keys` |
| `owner_env` | нет | Словарь переменных окружения, попадающих в `~/.env` |
## Про группу `docker`
Членство в группе `docker` равносильно root на хосте (`docker run -v /:/host`),
поэтому сервисным пользователям её не выдаём: контейнерами рулит Ansible от
root. Исключение — приложения, чей `backup.sh` дампит базу через
`docker compose exec`: оркестратор бэкапов запускает такие скрипты от имени
самого приложения. См. `docs/backlog/backup-dump-without-docker-group.md`.
## Пример использования
```yaml
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
```
+20
View File
@@ -0,0 +1,20 @@
---
galaxy_info:
role_name: owner
author: "Anton Vakhrushev"
description: "Системный пользователь приложения: группа, uid/gid, ssh-ключи и переменные окружения"
license: "MIT"
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions:
- all
- name: Debian
versions:
- all
galaxy_tags:
- users
- system
- environment
dependencies: []
+28 -20
View File
@@ -1,21 +1,24 @@
--- ---
- name: 'Check app requirements for user "{{ owner_name }}".' # tasks file for owner
ansible.builtin.fail:
msg: You must set owner name.
when: not owner_name
- name: 'Check app requirements for user "{{ owner_name }}".' - name: "Validate owner role arguments"
ansible.builtin.fail: ansible.builtin.assert:
msg: You must set owner uid. that:
when: not owner_uid - owner_name is string and owner_name | length > 0
- owner_uid | int > 0
- owner_gid | int > 0
fail_msg: >-
Роль owner требует непустой owner_name и положительные owner_uid/owner_gid
(owner_name={{ owner_name }}, owner_uid={{ owner_uid }}, owner_gid={{ owner_gid }})
quiet: true
- name: 'Create group "{{ owner_group }}".' - name: "Create group {{ owner_group }}"
ansible.builtin.group: ansible.builtin.group:
name: "{{ owner_group }}" name: "{{ owner_group }}"
gid: "{{ owner_gid }}" gid: "{{ owner_gid }}"
state: present state: present
- name: 'Create user "{{ owner_name }}".' - name: "Create user {{ owner_name }}"
ansible.builtin.user: ansible.builtin.user:
name: "{{ owner_name }}" name: "{{ owner_name }}"
group: "{{ owner_group }}" group: "{{ owner_group }}"
@@ -24,20 +27,21 @@
shell: /bin/bash shell: /bin/bash
register: owner_create_result register: owner_create_result
- name: 'Set up user ssh keys for user "{{ owner_name }}".' - name: "Set up ssh keys for user {{ owner_name }}"
ansible.posix.authorized_key: ansible.posix.authorized_key:
user: "{{ owner_name }}" user: "{{ owner_name }}"
key: "{{ item }}" key: "{{ item }}"
state: present state: present
with_items: "{{ owner_ssh_keys }}" loop: "{{ owner_ssh_keys }}"
when: owner_ssh_keys | length > 0
- name: "Prepare env variables." # USER_UID/USER_GID кладём в окружение всегда: их подставляют docker-compose
# шаблоны приложений, чтобы контейнер работал от того же пользователя.
- name: "Prepare env variables"
ansible.builtin.set_fact: ansible.builtin.set_fact:
# yamllint disable-line rule:line-length owner_env_dict: >-
owner_env_dict: '{{ owner_env | combine({"USER_UID": owner_create_result.uid, "USER_GID": owner_create_result.group}) }}' {{ owner_env | combine({"USER_UID": owner_create_result.uid, "USER_GID": owner_create_result.group}) }}
- name: 'Set up environment variables for user "{{ owner_name }}".' - name: "Set up environment variables for user {{ owner_name }}"
ansible.builtin.template: ansible.builtin.template:
src: env.template src: env.template
dest: "/home/{{ owner_name }}/.env" dest: "/home/{{ owner_name }}/.env"
@@ -45,14 +49,18 @@
group: "{{ owner_group }}" group: "{{ owner_group }}"
mode: "0640" mode: "0640"
- name: 'Remove from bashrc absent environment variables for user "{{ owner_name }}".' # Раньше переменные писались в .bashrc построчно; теперь источник истины —
# ~/.env, поэтому старые строки вычищаем.
- name: "Remove from bashrc absent environment variables for user {{ owner_name }}"
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: "/home/{{ owner_name }}/.bashrc" path: "/home/{{ owner_name }}/.bashrc"
regexp: "^export {{ item.key }}=" regexp: "^export {{ item.key }}="
state: absent state: absent
with_dict: "{{ owner_env_dict }}" loop: "{{ owner_env_dict | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: 'Include in bashrc environment variables for user "{{ owner_name }}".' - name: "Include in bashrc environment variables for user {{ owner_name }}"
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: "/home/{{ owner_name }}/.bashrc" path: "/home/{{ owner_name }}/.bashrc"
regexp: "^export \\$\\(grep -v" regexp: "^export \\$\\(grep -v"