From 534fa429e13f101242300bbf3597880272e3a752 Mon Sep 17 00:00:00 2001 From: oqyude Date: Wed, 7 Oct 2026 11:29:21 +0300 Subject: [PATCH] docs arch begin --- AGENTS.md | 137 ++++++++++++++ docs/arch/invariants.md | 195 ++++++++++++++++++++ docs/arch/map.md | 396 ++++++++++++++++++++++++++++++++++++++++ docs/arch/todo.md | 316 ++++++++++++++++++++++++++++++++ 4 files changed, 1044 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/arch/invariants.md create mode 100644 docs/arch/map.md create mode 100644 docs/arch/todo.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d03e5da --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,137 @@ +# AGENTS.md + +NixOS-конфиг домашнего флота. 6 NixOS-хостов + Android (`nix-on-droid`). + +Этот файл — то, что агент должен прочитать **до** первого изменения. Если задача +выглядит так, что требует сломать что-то из «Подтверждённых инвариантов» или +«Ловушек» ниже — остановиться и спросить. + +## Архитектура (30 секунд) + +``` +flake.nix +├── configurations/ ← реестр хостов (1 запись = 1 машина) +│ ├── default.nix ← hosts + xlibLib + mkSystem +│ ├── .nix ← модульное тело хоста +│ └── hardware/.nix +├── home/ ← home-manager (per device-type) +├── modules/ +│ ├── options.nix ← кросс-модульные опции +│ ├── default.nix ← defaultModule + strictModule (для nix-on-droid) +│ ├── essentials/ ← packages, services, settings, ssh, shell, systemd-routines +│ ├── desktop/, server/, server/├── vds/, wsl/, containers/, termux/, other/ +├── lib/ +│ ├── mkSystem.nix ← nixosSystem + specialArgs(xlib, inputs) +│ └── xlib/ ← чистые данные: devices, dirs, helpers +├── overlays/, pkgs/, deploy/, secrets/ (sops) +└── .sops.yaml ← один age-ключ на secrets/.(yaml|json|env|ini) +``` + +`xlib` (в `lib/xlib/`) — чистые данные: identity (`device`), capability flags, +директории, helper'ы. Передаётся в каждый модуль через `specialArgs`. Конфиг не +может переопределить `xlib` — единственная точка изменения это `configurations/default.nix`. + +## Хосты + +| Attr / имя | device.type | Роль | Деплой | Примечание | +|---|---|---|---|---| +| `default` (nixos) | minimal | Шаблон / минималка | — | hostname `"nixos"` | +| `atoridu` | primary | Основной десктоп | — | xanmod | +| `rydiwo` | secondary | Ноутбук Chuwi MiniBook (xanmod, NTFS) | deploy-rs | `stateVersion 26.05` | +| `otrecа` | vds | VPS, SSH только по Tailscale | deploy-rs | nftables, DHCP, no firewall в NixOS | +| `sapphira` | server | Домашний сервер (белый IP через роутер) | deploy-rs | `firewall.enable = false` намеренно | +| `wsl` | wsl | WSL NixOS на vetymae | — | nixos-wsl module | +| `epral` | termux | Android (`nix-on-droid`) | — | через `mobile.nix`, отдельный модульный путь | + +`device.type` ∈ { minimal, primary, secondary, server, vds, wsl, termux }. +`modules/defaultModule` импортирует `modules//` через `lib.optional +(!isDesktop && type != "minimal") (./. + "/${type}")`. + +## Подтверждённые инварианты + +1. **Все `outputs` флейка должны вычисляться.** `configurations/mobile.nix:12` + импортировал несуществующий `lib/xlib.nix` — был сломан, `epral` не + собирался. Зафиксировать через `nix flake check`. +2. **Носитель данных (`/home/oqyude/External`) обязан быть смонтирован** до + старта `postgresql`, `n8n`, `samba`, `homebox`, `minecraft`, `3x-ui`, + `tape-rotation`. `mkServiceStorage` даёт `bind,x-systemd.automount,nofail` + — без guard'а сервис стартует на пустой БД. → todo B1. +3. **Сетевая граница sapphira — роутер.** `firewall.enable = false` намеренно. + Роутер пробрасывает ровно 5 портов: **443, 80, 22000 (syncthing), 8443 + (xray), 22 (ssh)**. `nginx.nix:225` (`allowedTCPPorts = [80 443]`) мёртв. + `openFirewall`/`allowedTCPPorts` на sapphira не имеют эффекта. +5. **`100.64.0.0` = Tailscale-адрес sapphira**, назначен вручную. Не сеть, не + ошибка. Используется в `nginx.nix`, `nextcloud.nix` (`trusted_proxies`), + `vds/systemd.nix`, `vds/nginx.nix`. При смене — править 4 файла. +4. **3x-ui заморожен.** Панель на последней версии (образ `:latest` → запинить), + ядро Xray на 26.7.x. Миграция на 26.9.x провалена. Обходные скрипты (тimer, + migrateScript) отключены осознанно. **Не** обновлять ядро через панель без + записи в `docs/arch/notes/3x-ui-xray-26.9.md`. +6. **nftables на VDS требует явной финальной политики.** Текущий ruleset + (`vds.nix:73-91`) — без явного последнего правила и без `policy` → неявный + accept. На otreca одновременно `nftables.enable = true` и `firewall.*` — + проверить, кто реально владеет ruleset'ом, перед правкой. + +## Ловушки (выглядит сломанным, намеренно) + +| Где | Что выглядит ошибкой | На самом деле | +|---|---|---| +| `server.nix:130` | `firewall.enable = false` при 20 сервисах на `0.0.0.0` | Роутер фильтрует, см. §4 | +| `mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 vs 26.05 | Каждый хост зафиксирован на своей версии | +| `users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` | Костыль под 1000 = удалённый `yuyus`; удалять только после миграции ФС | +| `3x-ui.nix:54` | `image = …:latest` | Панель намеренно latest; ядро Xray — на 26.7.x | +| `3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS | Следствие отката `c8d4a12`; смысл утрачен, см. todo C5 | +| `server/default.nix:33-47` | 15 закомментированных модулей | Отключены осознанно, см. todo E3 | +| `opencode.nix:339` | `systemd.user.services.opencode-web.Service` | `serviceConfig` рендерится в секцию `[serviceConfig]`, systemd молча игнорирует (`c73a698`) | +| `vds.nix:73-91` | nftables без финального правила | Известный пробел, см. todo A3 | +| `100.64.0.0` | Первый адрес CGNAT `/10` | Tassigned вручную, см. §5 | +| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. todo B1 | + +## Куда лезть по задаче + +| Задача | Файл | +|---|---| +| Добавить хост | `configurations/default.nix` + `configurations/.nix` + `configurations/{hardware,disko}/.nix` | +| Добавить системный сервис | `modules/server/.nix`, добавить в `modules/server/default.nix:imports` | +| Добавить home-пакет для пользователя | `home/.nix` (через `lib.mkIf` или просто список) | +| Добавить опцию, читаемую несколькими модулями | `modules/options.nix` | +| Изменить mount/имя пользователя | `lib/xlib/dirs.nix`, `lib/xlib/device.nix` | +| Изменить домен / сертификат | `modules/server/coredns.nix` + `modules/server/nginx.nix` (или `vds/`) | +| Sops-секрет | положить в `secrets/.`; `users.nix:99` уже подключает `secrets/default.yaml`; dotenv/json-секреты — через `mkUserSecret` | + +## Проверки + +```bash +# все outputs вычисляются +nix flake check + +# правки применились на целевой хост +nix build .#nixosConfigurations..config.system.build.toplevel + +# nixOnDroid +nix build .#nixOnDroidConfigurations.epral.config.system.build.toplevel + +# внешний диск смонтирован (до рестарта сервисов на нём) +findmnt /home/oqyude/External +findmnt /mnt/services + +# state of guard-зависимостей (когда будет todo B1) +systemctl show postgresql -p Requires -p After | tr ' ' '\n' | grep -E 'mnt-|home-oqyude' + +# sops +sops --version +``` + +## Где НЕ лезть без ответа владельца + +- `secrets/` (sops-encrypted, расшифровываются `/etc/ssh/id_ed25519` → циклический bootstrap). +- `lдet deploy` без проверки deploy-rs нод: `rydiwo` (ноутбук, может быть выключен). +- Любая правка, противоречащая «Подтверждённым инвариантам» выше. + +## Дальше читать + +- `docs/arch/map.md` — полная карта: per-host детали, сетевая топология, + инвентарь сервисов, все известные open questions. +- `docs/arch/invariants.md` — слои 9–11 (home-manager, deploy, формат) + + полный список неотвеченных вопросов слоёв 1–8. +- `docs/arch/todo.md` — задачи A1–F (правки и документирование). \ No newline at end of file diff --git a/docs/arch/invariants.md b/docs/arch/invariants.md new file mode 100644 index 0000000..a75225a --- /dev/null +++ b/docs/arch/invariants.md @@ -0,0 +1,195 @@ +# Инварианты: вопросы владельцу + +Проход по репозиторию сверху вниз, 2026-10-05. 120 `.nix`, ~8.5k строк. + +Структура: +- Сводный ответ, ядро и ловушки → **`AGENTS.md`** (корень репозитория). +- Подробная карта архитектуры с per-host деталями и инвентарём сервисов → + **`docs/arch/map.md`**. +- Этот файл → **открытые вопросы** (слои 0–8) + ещё непрочитанные **слои 9–11** + (home-manager, deploy, формат). + +Пометки: `[!]` — найденный дефект, не вопрос. `[?]` — не смог определить по коду. +`[✓]` — отвечено владельцем 2026-10-05. + +## Статус ответов (2026-10-05) + +Отвечено: **2.1, 5.2, 6.1, 6.3, 6.5, 7.1, 7.2** (7 пунктов). Остальные ждут +ответа (таблица ниже). + +Ключевое из ответов, что меняет картину: + +- **6.5 — моя ошибка.** `100.64.0.0` не «сетевой адрес вместо интерфейса»: + это Tailscale-адрес sapphira, назначенный вручную. +- **6.3 — это не дыра, а осознанное решение.** Граница держится на роутере: + на сервер пробрасываются ровно 5 портов — **443, 80, 22000 (syncthing), + 8443 (xray), 22 (ssh)**. `firewall.enable = false` на sapphira — следствие, + а не недосмотр. Проблема в другом: **список пробросов нигде не записан + в репозитории**, и именно его агент обязан уважать (D1 в `todo.md`). +- **7.2 — 3x-ui рабочий.** Откат сделан осознанно: панель последняя, ядро Xray + осталось на 26.7.28, миграция на 26.9 провалена, лишний код закомментирован. + Состояние — «заморожено», а не «сломано». +- **5.2 — подтверждённая дыра в защите данных.** Guard для несмонтированного + носителя не был продуман → задача B1. + +## Сводка подтверждённых инвариантов + +| # | Пункт | Краткая формулировка | См. | +|---|---|---|---| +| 1 | Все `outputs` флейка вычисляются | A1: правка `lib/xlib.nix` → `lib/xlib`; закрепить через `nix flake check` | todo A1 | +| 2 | External-диск монтируется до сервисов | mkServiceStorage + bind без guard'а → сервис стартует на пустой БД | todo B1 | +| 3 | Сетевая граница sapphira = роутер | 5 портов: 22, 80, 443, 8443, 22000; `firewall.enable = false` намеренно | todo D1 | +| 4 | `100.64.0.0` = Tailscale sapphira | Назначен вручную; в 4 файлах | AGENTS.md §5 | +| 5 | 3x-ui заморожен | Панель на latest; ядро Xray на 26.7.x; миграция 26.9 провалена | todo C1–C5 | +| 6 | nftables на VDS — явная финальная политика | Сейчас ruleset без финального правила + конфликт с `firewall.*` | todo A3 | + +## Сводка по ловушкам + +Полная таблица (10 пунктов) в **`AGENTS.md`** → раздел «Ловушки». Кратко: +`firewall.enable=false` намеренно · uid=1001 на sapphira · `image = …:latest` +намеренно для 3x-ui · `reality443Forwarding=true` — следствие отката · +15 закомментированных модулей в server/default.nix · `serviceConfig` vs `Service` +в home-manager · nftables без финального правила · `100.64.0.0` не сеть · +`/mnt/services` mode 0777 · `stateVersion` разный между хостами. + +## Неотвеченные вопросы (слои 0–8) + +Самые важные — выделены. + +| ID | Вопрос | Что блокирует | +|---|---|---| +| 0.1 | Восстанавливать ли migration notes, удалённые в `22a19be`? | C1: реконструкция заметки 3x-ui | +| 0.2 | Комментарий-density 38/120 файлов без комментариев — нормально? | Стиль модулей | +| 0.3 | 15 закомментированных модулей: удалить или хранить как референс? | E3: чистота кода | +| 0.4 | README пустой, todo.md нет — норма? | E1: AGENTS.md/README | +| 1.3 | Лишние inputs в flake (`justray`, `nix-minecraft`, `proxy-suite`)? | Чистота flake | +| 1.5 | `nix-systems` через `follows` — оптимизация размера lock | Документация | +| **2.2** | **`vetymae` / `lamet` / `therima` / `soptur` — те же машины или хосты вне реестра?** | **DNS/nginx/identity** | +| 2.3 | sapphira uid=1001: блокер ли использование `/mnt/archive`/`/mnt/mobile`? | Миграция ФС | +| **2.5** | **stateVersion 24.05 / 24.11 / 25.05 / 26.05 — намеренный дрейф?** | **Миграции** | +| 2.6 | Есть ли escape hatch для per-host отличий в xlib? | Архитектура | +| 2.7 | `devices.termux` без NixOS-хоста — закрытый список | Документация | +| 3.2 | `any.nix` (minimal) нужны home-manager + sops + disko? | Минималка | +| **4.1** | **Как root получает доступ по SSH — authorizedKeys в коде нет** | **deploy, безопасность** | +| **4.2** | **Как разрешается цикл «ключ в секрете, а нужен для расшифровки»?** | **bootstrap, recovery** | +| 4.3 | Все файлы в `secrets/` покрыты `path_regex`? | sops | +| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе? | sops, scale | +| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя»? | Безопасность | +| 5.1 | `/mnt/services` mode 0777 — осознанно? | Безопасность | +| 5.3 | NFS выключен, Samba работает — миграция? | Сетевые сервисы | +| 5.4 | NTFS-том `lamet-drive` `mask=0000` — что на нём? | Семантика | +| 5.5 | `therima` / `vetymae` / `soptur` в dirs.nix — реально смонтированы? | Семантика | +| 5.6 | Где бэкапы БД и 3x-ui? | B2 | +| 6.6 | `192.168.1.20` зашит в 30 мест — константа? | Рефакторинг | +| 6.7 | DNS ↔ сервисы — как ловим рассинхрон? | Документация, CI | +| **6.8** | **Публичные IP + SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна?** | **Архитектура** | +| 6.9 | Какой путь REALITY правильный сейчас? | C5 | +| 7.4 | Почему не публиковать весь диапазон 14380-15379? | 3x-ui | +| 8.2 | `lamet.opencodes` → `:6061` (порт miniflux) — ошибка? | nginx | +| 8.4 | `onlyoffice` после трёх регрессов — работает? | Статус сервиса | +| 8.5 | Что слушает `:3002` (`/whiteboard` nextcloud)? | Карта сервисов | +| 8.6 | Бэкапы вне Nix — записать | Документация | + +## Где это раньше лежало + +До переноса в `AGENTS.md` / `map.md` здесь был подробный Q&A по слоям 0–8 +с разделами «Вопрос», «Факт», «Риск», «Кандидат». Этот текст сохранён в +git-истории файла (последний коммит, где Q&A был полным). Восстановить: +`git log -p docs/arch/invariants.md | less`. + +--- + +## Слой 9. home-manager + +**9.1** `home/home.nix:52-57` — для пользователя импортируется +`home/${xlib.device.type}.nix`; для `root` — без профиля (строка 51). +**Вопрос:** почему у `root` нет home-профиля — сознательно? +**Кандидат:** `home/.nix` = единственный источник «что есть на этом хосте» +для пользователя; добавление пакета в новый тип = правильный файл, а не +`home/default.nix`. + +**9.2 [!]** `home/home.nix:28-43` для headless-хостов: `xdg.userDirs.* = null` и +`createDirectories = false`, при этом `lib/xlib/dirs.nix:26` обещает +`music-library = "${user-home}/Music"`. +**Вопрос:** кто создаёт `~/Music` и `~/Storage`? `createDirectories = false` +означает, что home-manager их не создаст, а `dirs.nix` на них ссылается. +**Риск:** на headless-хосте путь в конфиге есть, а каталога нет → тихий сбой +сервиса, который туда пишет. + +**9.3 [!]** `home/modules/opencode.nix:339-350` (`c73a698`): в home-manager нельзя +писать `serviceConfig = { ... }` — рендерится литеральная секция `[serviceConfig]`, +которую systemd молча игнорирует («Unknown section 'serviceConfig'. Ignoring.»). +Правильно: `systemd.user.services.opencode-web.Service = { ... }`. +**Кандидат (готовый инвариант, стоит закрепить буквально в `AGENTS.md`):** +в home-manager cgroup-опции (`MemoryHigh`, `OOMScoreAdjust`, …) пишутся +в `systemd.user.services..Service`, **не** в `serviceConfig`. Ошибка +не диагностируется — она просто не применяется. + +**9.4** `linger = true` добавлен ради `opencode-web` (`users.nix:71-75`) и включён +**для всех** хостов. +**Кандидат:** «user-сервисы переживают logout на всех хостах» — закрепить, потому +что это неочевидное поведение, влияющее на ресурсы и на безопасность. + +**9.5** `home/modules/opencode.nix:286-303` — `opencode.web` слушает `0.0.0.0:4096` +(комментарий: nginx проксирует `127.0.0.1:4096`), и nginx на sapphira ходит туда +же по Tailscale у двух других хостов (см. 8.3). +**Кандидат:** `0.0.0.0` в `opencode.web` — обязательное условие для внешнего +доступа через `opencodes.*`; пароль приходит из sops-секрета `opencode_server`. + +**9.6** Секреты opencode приходят в `~/.config/opencode/server.env` (dotenv), +`~/.local/share/opencode/auth.json` и `account.json` (json, `key = ""`). +**Кандидат:** эти три файла перезаписываются sops при каждой активации — ручные +правки в них теряются. Уже отражено в комментарии `users.nix:120-131`, стоит +закрепить как инвариант. + +--- + +## Слой 10. deploy и проверка + +**10.1** `deploy/default.nix:20-24` — цели: `sapphira` (server), `otrecа` (vds), +`rydiwo` (ноутбук). **Нет** `atoridu` (основной десктоп), `wsl`, `epral`. +**Вопрос:** почему не деплоится десктоп? И безопасно ли пересобирать ноутбук +`rydiwo` по SSH (он может быть выключен/на другом Wi-Fi)? +**Кандидат:** `deploy-rs` = только серверы + ноутбук; десктоп и WSL обновляются +вручную. Инвариант: не добавлять в `deploy.nodes` хост, который нельзя +пересобрать в любой момент без риска потерять доступ. + +**10.2** `deploy/default.nix:18-19` — `sshUser = "oqyude"`, `user = "root"`. +См. 4.1: root-доход по SSH не описан в конфигурации. +**Кандидат:** деплой требует ручной настройки root-доступа на каждом из 3 хостов — +это скрытая зависимость, которую агент не выведет. + +**10.3** `deploy/default.nix:27-29` — `checks = builtins.mapAttrs (... deployChecks)`. +**Вопрос:** `nix flake check` реально проходит сейчас? Учитывая 2.1 (`lib/xlib.nix`) +он должен падать на `nixOnDroidConfigurations`. Падает или `checks` покрывают +не всё дерево outputs? +**Кандидат (первое, что стоит сделать):** добиться, чтобы +`nix flake check` был зелёным — это единственная автоматическая защита от +подобных breakage'ов. + +**10.4** CI нет, `flake check` не запускается автоматически. +**Кандидат:** минимальный локальный набор перед коммитом: +`nix flake check && nix build .#nixosConfigurations.<хост>.config.system.build.toplevel --dry-run`. + +--- + +## Слой 11. Формат (то, что я предлагаю зафиксировать как процесс) + +**11.1** Где будет жить итог: `AGENTS.md` в корне (читается агентом всегда), +`docs/arch/map.md` (карта хостов/сервисов), `docs/arch/invariants.md` (этот файл). +**Кандидат:** этот файл после ответов превращается в `docs/arch/invariants.md` +с колонкой «ответ» и становится источником для `AGENTS.md`; `AGENTS.md` — краткая +выжимка, без подробностей. + +**11.2** Какие инварианты можно превратить в автоматическую проверку (тогда они +перестанут «забываться»): +1. ни одного `:latest` в образах (grep по `image =`); +2. `nix flake check` зелёный; +3. каждый домен из `coredns.nix` имеет vhost в `nginx.nix` и наоборот; +4. каждый сервис в `mkServiceStorage` имеет каталог в `/mnt/services` на + `External`-диске; +5. в самописном nftables-ruleset последнее правило цепочки явное; +6. каждый `listen.addr` — реально назначенный адрес, а не сеть; +7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`. + +**Вопрос:** какие из этих проверок ты хочешь, а какие — лишний CI? \ No newline at end of file diff --git a/docs/arch/map.md b/docs/arch/map.md new file mode 100644 index 0000000..8269a1f --- /dev/null +++ b/docs/arch/map.md @@ -0,0 +1,396 @@ +# Карта архитектуры + +Полная карта репозитория: per-host детали, сетевая топология, инвентарь сервисов. +Слои 0–8 проработаны; слои 9–11 (home-manager, deploy, формат) см. в +`docs/arch/invariants.md`. + +## Содержание + +1. [Реестр хостов](#реестр-хостов) +2. [Идентичность и xlib](#идентичность-и-xlib) +3. [Диспетчеризация модулей](#диспетчеризация-модулей) +4. [Пользователь, SSH, секреты](#пользователь-ssh-секреты) +5. [Хранилище](#хранилище) +6. [Сеть и firewall](#сеть-и-firewall) +7. [Сервисы](#сервисы) +8. [Неотвеченные вопросы](#неотвеченные-вопросы) + +--- + +## Реестр хостов + +Единственная точка добавления/изменения хоста — `configurations/default.nix:13-39`. +Имя атрибута **равно** hostname; отдельное `hostname = …` только у `default` +(где attr = `default`). + +| Attr | hostname | device.type | description | +|---|---|---|---| +| `default` | nixos | minimal | Шаблон, hostname `"nixos"`, `device = "minimal"` | +| `atoridu` | atoridu | primary | Основной десктоп, `xanmod` | +| `rydiwo` | rydiwo | secondary | Chuwi MiniBook, `xanmod`, NTFS-том `lamet-drive` | +| `otrecа` | otreca | vds | VPS, SSH только через Tailscale, `grub` без EFI | +| `sapphira` | sapphira | server | Домашний сервер, `firewall.enable = false` намеренно | +| `wsl` | wsl | wsl | WSL NixOS на Windows-хосте `vetymae` | +| `epral` | epral | termux | Android (`nix-on-droid`), отдельный путь конфигурации | + +`device.type` ∈ { minimal, primary, secondary, server, vds, wsl, termux }. +Машина `vetymae` (Windows + WSL) фигурирует в `coredns`, `nginx`, `modules/server/systemd.nix`, +но **не** в реестре хостов — это внешний хост, через который заходят на WSL. + +### Per-host summary + +- **`atoridu`** (`primary/mini-pc`): без `nixos-hardware` (мини-ПК). Linux `xanmod_stable`, + `systemd-boot`, EFI. `stateVersion 26.05`. → `configurations/mini-pc.nix`. +- **`rydiwo`** (`secondary/mini-laptop`): `nixos-hardware: chuwi-minibook-x`, xanmod, + `systemd-boot`, EFI. **NTFS-том `xlib.dirs.lamet-drive`** с `mask = "0000"` — + world-readable/writable по дизайну [?]. `stateVersion 26.05`. +- **`otrecа`** (`vds`): qemu-guest, GRUB без EFI, `disko` + `hardware/vds.nix`. + `firewall.enable = true` + ручной `nftables.ruleset` без финального правила. + `firewall.interfaces.tailscale0.allowedTCPPorts = [22]`. `stateVersion 25.05`. +- **`sapphira`** (`server`): systemd-boot, EFI, ext4 на UUID `37e53ebc-…-a8de`. + bind-mount `/mnt/services` ← `/home/oqyude/External/Services`. `stateVersion 25.05`. +- **`wsl`**: `nixos-wsl` + NixOS-стек, IPv6 on, `firewall.enable = false`. + `stateVersion 24.11`. Реальный Windows-хост — `vetymae`, `192.168.1.100`. +- **`epral`** (`mobile.nix`): не NixOS, **nix-on-droid**. `stateVersion 24.05`. + +--- + +## Идентичность и xlib + +`lib/xlib/` собирает чистые данные (без модулей): + +``` +xlib = { + device = { hostname, type, username, uid, gid }; + isDesktop, isHeadless; # ← от device.type через devices..{desktop,headless} + dirs = mkDirs username; # ← well-known пути, зависят только от username + helpers = { mkBindMount, mkSystemdBind, mkServiceStorage, mkNtfsMount, + mkExfatMount, mkTmpDirs, mkSymlinks }; +} +``` + +- **`mkXlib`** (`lib/xlib/default.nix:38-77`) — единственная точка сборки; вызывается + в `configurations/default.nix:50`. Прокидывается в каждый модуль как + `xlib = …` через `lib/mkSystem.nix:specialArgs`. +- **`devices`** (`lib/xlib/device.nix:12-41`) — закрытое множество device.types. + Неизвестный тип → throw со списком валидных. Добавление типа = новая папка + `modules//` + `home/.nix` + запись в `devices`. +- **`uid/gid`** зашиты как `?` `1000`/`1000` в `mkXlib`. Менять = инвентаризация + во всех хостах, иначе расходятся владельцы файлов на NTFS/exFAT. +- **`sapphira`** — исключение: `users.nix:66` ставит `uid = 1001` для сохранения + совместимости со старым `uid-map` (`yuyus` = 1000). `TODO: delete once + sapphira migrated to 1000`. Цена: exFAT на sapphira получает `uid=1000` от + `xlib.device.uid`, поэтому пользователь не может писать в `/mnt/archive` и + `/mnt/mobile` до миграции. + +--- + +## Диспетчеризация модулей + +### `nixosModules.default` (`modules/default.nix:9-39`) + +Импортирует на **каждый** NixOS-хост (включая `minimal`): + +``` +./essentials → packages, services, settings, ssh, shell, systemd-routines +./options.nix → host.builder.*, host."3x-ui".* +./users.nix → пользователь, sops-секреты +home-manager.nixosModules.home-manager +sops-nix.nixosModules.sops +justray.nixosModules.default +disko.nixosModules.disko +grub2-themes.nixosModules.default +self.homeConfigurations.default.nixosModule +``` + +Плюс `lib.optional xlib.isDesktop ./desktop` (primary, secondary). +Плюс `lib.optional (!isDesktop && type != "minimal") (./. + "/${type}")` +(server, vds, wsl, termux). **termux** попадает сюда только в path nix-on-droid, +не как NixOS-хост (см. `mobile.nix`). + +### `nixosModules.strict` (`modules/default.nix:40-53`) + +Используется только `mobile.nix:22`. Импортирует `options.nix` + +`./`; **всё** остальное NixOS-специфичное (essentials, users, +home-manager, sops, disko, grub2-themes) **выключено**, потому что nix-on-droid +не имеет `services.*`, `users.*`, `sops.*`, `disko.*` в своей модульной системе. + +### Правило для кросс-модульных опций + +Опция живёт в `modules/options.nix`, если её **устанавливает** один модуль, +а **читает** другой. `host.reader.X.enable` живёт в `essentials/ssh.nix`, потому +что его объявляет и использует один модуль. + +--- + +## Пользователь, SSH, секреты + +### Пользователь `oqyude` + +- `uid` = `1000` на всех хостах, кроме `sapphira` (=`1001`, см. выше). +- `home = /home/oqyude`, `homeMode = "700"`. +- `linger = true` на всех хостах — user-services (opencode-web) переживают logout. + Следствие: user-сервисы стартуют и потребляют ресурсы без активной сессии. +- `extraGroups`: `audio disk gamemode networkmanager pipewire wheel libvirtd qemu-libvirtd`. + +### SSH + +- `essentials/ssh.nix`: `services.openssh` включается через `host.ssh.enable`, + `PermitRootLogin = "yes"` (намеренно для deploy), `PasswordAuthentication = false`, + hostKey = `/etc/ssh/id_ed25519`. +- `authorizedKeys` для `oqyude` зашит в `users.nix:87` (`ssh-ed25519 AAAA…`). + Чей — `[?]` (см. вопрос 4.1). +- `users.nix` определяет `root`-authorizedKeys **отсутствует** [?] — root как-то + попадает на хост; deploy-rs использует `sshUser = "oqyude", user = "root"`. + +### Циклическая зависимость ключа + +`/etc/ssh/id_ed25519` одновременно: +- `hostKeys` для sshd (`essentials/ssh.nix:22`) +- `sops.age.sshKeyPaths` для расшифровки (`users.nix:95-97`) +- цель `ssh_key_private_known` (`users.nix:147-152`) +- цель `ssh_key_public_host` (`users.nix:159`) + +Как разворачивается на чистой машине — **одноразовый bootstrap** [?]. +Должен быть задокументирован, иначе при переустановке хоста агент не выведет. + +### `.sops.yaml` + +- Один age-ключ (`*default`), `path_regex: secrets/[^/]+\.(yaml|json|env|ini)$`. +- Покрывает только плоские файлы в `secrets/` (без подкаталогов). +- Добавление секрета = `secrets/<имя>.` строго в корне. +- Дополнительные секреты dotenv/json — через `mkUserSecret` (`users.nix:33-41`). + +### Инвентарь секретов (`users.nix:100-162`) + +| Секрет | Формат | Назначение | +|---|---|---| +| `hashed_password` | yaml | Пароль пользователя | +| `age_key_private` | yaml | `~/.config/sops/age/keys.txt` | +| `opencode_server` | dotenv | `~/.config/opencode/server.env` | +| `opencode_auth` | json | `~/.local/share/opencode/auth.json` (`key=""`) | +| `opencode_account` | json | `~/.local/share/opencode/account.json` (`key=""`) | +| `ssh_key_private` | yaml | `~/.ssh/id_ed25519` | +| `ssh_key_public` | yaml | `~/.ssh/id_ed25519.pub` | +| `ssh_key_private_root` | yaml | `/root/.ssh/id_ed25519` | +| `ssh_key_public_root` | yaml | `/root/.ssh/id_ed25519.pub` | +| `ssh_key_public_host` | yaml | `/etc/ssh/id_ed25519.pub` | + +--- + +## Хранилище + +### `/home/oqyude/External` (ext4) + +- `sapphira`: UUID `37e53ebc-5343-a94d-9fe2-0ca39e13a8de`, fsType `ext4`, + **без `nofail`**, **без automount** — обычный mount, без `x-systemd.automount`, + не помечен как `requiredBy local-fs.target` явно, но NixOS добавляет это для + всех `fileSystems` без `nofail` [?]. +- `rydiwo`: не смонтирован (у ноутбука есть только NTFS `lamet-drive`). +- На других NixOS-хостах — не заявлен (нет внешнего диска). + +### `/mnt/services` (bind) + +- `server.nix:49-52`: `mkBindMount` от `xlib.dirs.services-folder` + (= `/home/oqyude/External/Services`) к `/mnt/services`, `bind,nofail`. +- `server.nix:61-63`: tmpfiles `z /mnt/services 0777 root root`. +- `vds/default.nix:23`: tmpfiles создаёт `/mnt/services` с правами `0755`. +- Используется сервисами на sapphira для bind-mount сервисных данных + (`mkServiceStorage`) и как прямой `stateDir` для gitea/memos/calibre-web/ + immich/nextcloud/step-ca/trilium/uptime-kuma/3x-ui/tape-rotation. + +### `/mnt/archive`, `/mnt/mobile`, `/mnt/lamet`, `/mnt/therima`, `/mnt/vetymae`, `/mnt/soptur` + +- `archive` и `mobile` смонтированы на sapphira через `mkExfatMount` + (`nofail`+uid=1000). +- `lamet` — NTFS на rydiwo (`mask = "0000"`). +- `therima`, `vetymae`, `soptur` — **не** смонтированы нигде в репозитории + (см. вопрос 2.2). +- `dirs.nix` объявляет их все; `dirs.nix` **не** читать как список дисков этой + системы — там имена, часть из которых не существует. + +### Потребители External-диска и порядок защиты + +Включённые на sapphira сервисы с данными на `/mnt/services` или `/home/oqyude/External`: + +- `postgresql`, `samba-smbd`, `homebox` (+setup), `gitea` (+dump), + `navidrome`, `syncthing`, `uptime-kuma`, `immich-server` (+ML), + `nextcloud`, `calibre-web`, `podman-3xui_app`, `podman-tape-rotation` + +Все они обязаны иметь guard на `requiresMountsFor` (задача **B1** в `todo.md`). +Сейчас guard есть **только** у rsync-юнитов (`modules/server/systemd.nix:14,36`), +которые используют `--delete` и потенциально самые опасные при отсутствующем +диске. + +--- + +## Сеть и firewall + +### Топология + +``` +Интернет (роутер, белый IP) + ├── router NAT/proxy → sapphira: 443, 80, 22000, 8443, 22 (5 портов) + │ + └── otreca (VPS): SSH только через Tailscale, не пробрасываем + +LAN (192.168.1.0/24) + ├── 192.168.1.20 = sapphira (домашний сервер) + ├── 192.168.1.1 = роутер (gateway) + ├── 192.168.1.100 = vetymae (Windows-хост; на нём — WSL NixOS = `wsl`) + ├── 192.168.1.101, .102 = соседние машины (rsync/таблица в `termux.nix`) + └── ... + +Tailscale (CGNAT 100.64.0.0/10) + ├── 100.64.0.0 = sapphira (назначен вручную) + ├── 100.64.1.0 = ещё один узел [?] + ├── 100.86.62.4 = opencode на vetymae + └── 100.106.21.39 = miniflux на другом узле +``` + +`192.168.1.20` зашит в ~30 местах: `modules/server/{nginx,coredns,nfs,open-webui}.nix`, +`configurations/*`. `100.64.0.0` — в `nginx.nix`, `nextcloud.nix`, +`modules/vds/{nginx,systemd}.nix`. + +### DNS (`modules/server/coredns.nix`) + +Зоны `zeroq.su` (~17 записей) и `home.arpa` (~17) определены вручную. +Дублируют инвентарь сервисов: добавление сервиса = правка `coredns.nix` + +`nginx.nix` + самого модуля. + +### Firewall + +| Хост | `firewall.enable` | Фильтрация | +|---|---|---| +| sapphira | **false** (намеренно) | Роутер пробрасывает 5 портов: **443, 80, 22000, 8443, 22** | +| otreca | true | Самописанный nftables **без финального правила** → неявный accept; `firewall.interfaces.tailscale0.allowedTCPPorts = [22]` | +| wsl | false | WSL — не сетевой периметр | +| rydiwo, atoridu | default | `desktop` правила | + +Следствия: +- На `sapphira` `openFirewall`/`allowedTCPPorts` не имеют эффекта. +- `nginx.nix:225` (`allowedTCPPorts = [80 443]`) — **мёртвое** правило. +- Допустимо `0.0.0.0` на любом сервисе sapphira — он не открывается в интернет + без проброса на роутере. +- На `otrecа` ruleset требует финальной политики (задача A3). + +### SSH + +`otreca` достижима только через Tailscale: `services.openssh.openFirewall = false`, +`firewall.interfaces.tailscale0.allowedTCPPorts = [22]`. Но при `nftables.enable` +с ручным ruleset это правило может не дойти до файрвола — проверить +`nft list ruleset` на otreca до правок (задача A3). + +--- + +## Сервисы + +### Системные (sapphira, в `modules/server/default.nix:imports`) + +Сервисы в `imports` + `state` + `roles`: + +| Сервис | Файл | Порт | Данные | Guard? | +|---|---|---|---|---| +| acme (Let's Encrypt) | `modules/server/acme.nix` | — | `/var/lib/acme` | — | +| bentopdf | `bentopdf.nix` | — | — | — | +| builder (remote) | `builder.nix` | — | — | — (опция выключена) | +| calibre-web | `calibre-web.nix` | 8083 | `services-mnt-folder/calibre-web(-library)` | нужен B1 | +| chrony | `chrony.nix` | — | — | — | +| coredns | `coredns.nix` | 53 | inline zone | — | +| gitea | `gitea.nix` | 3000 | `services-mnt-folder/gitea` | нужен B1 | +| glances | `glances.nix` | — | — | — | +| homebox | `homebox.nix` | 7745 | `mkServiceStorage` | нужен B1 | +| immich | `immich.nix` | 2283 | `services-mnt-folder/immich` | нужен B1 | +| miniflux | `miniflux.nix` | 6061 | — | — | +| navidrome | `navidrome.nix` | 4533 | `server-home/Music` | нужен B1 | +| nextcloud | `nextcloud.nix` | 10000 | `services-mnt-folder/nextcloud` | нужен B1 | +| nginx | `nginx.nix` | 80/443 | proxy-only | — | +| nix-serve | `nix-serve.nix` | 5000 | — | — | +| onlyoffice | `onlyoffice.nix` | (через nginx) | — | — | +| postgresql | `postgresql.nix` | (local) | `mkServiceStorage` | нужен B1 | +| power | `power.nix` | — | — | — | +| samba | `samba.nix` | ? | `mkServiceStorage` | нужен B1 | +| syncthing | `syncthing.nix` | 8384 (gui), 22000 (data) | `server-home`, `storage/persist/...` | нужен B1 | +| systemd (rsync oneshots) | `systemd.nix` | — | источник/приёмник — оба на External | **уже есть guard** | +| uptime-kuma | `uptime-kuma.nix` | 4001 | `services-mnt-folder/uptime-kuma` | нужен B1 | + +Закомментированы в `imports` (всё ещё живой код, потенциальный шум): +`remnawave, coturn, mealie, memos, minecraft, n8n, netdata, nfs, open-webui, +rsync, step-ca, stirling-pdf, transmission, trilium, zerotier` — см. задачу **E3**. + +### Контейнеры (`modules/containers/`) + +| Контейнер | Файл | Данные | Примечание | +|---|---|---|---| +| 3x-ui | `3x-ui.nix` | `services-nodes-folder//3x-ui/{db,cert}` | **Заморожен**, см. ниже | +| tape-rotation | `tape-rotation.nix` | `services-nodes-folder//tape-rotation` | — | +| remnawave | `remnawave.nix` | `/mnt/services/containers/remnawave` | **закомментирован** в `server/default.nix` | +| remnanode | `remnanode.nix` | `/mnt/services/containers/remnanode` | — | +| kokoro-tts | `kokoro-tts.nix` | — | — | +| openhands | `openhands.nix` | — | — | +| remnawave-examples | `remnawave-examples/*.nix` | docker-compose | шаблоны | + +### 3x-ui — замороженное состояние + +Образ: `ghcr.io/mhsanaei/3x-ui:latest` (**не запинен**). Ядро Xray — на 26.7.x, +миграция на 26.9.x провалена. Панель может обновиться из upstream — поэтому: +- `podman-update-3xui_app` (`3x-ui.nix:80-90`) с `podman pull …:latest` + + `systemctl restart` — **таймер закомментирован**. +- `podman.autoPrune.flags = ["--all"]` (`3x-ui.nix:45-47`) — потенциальный риск: + авто-prune может смести панель без коммита в репозиторий. + +`reality443Forwarding = true` (`modules/vds/default.nix:19`) — следствие отката +`c8d4a12`; смысл утрачен, см. задачу **C5**. + +### Nginx (`modules/server/nginx.nix`) + +~12 vhost'ов через `mkProxy` для обратного проксирования сервисов на 192.168.1.20. +Плюс несколько hand-written: + +- `nextcloud.private` — слушает на `100.64.0.0:10000` (= Tailscale sapphira), + `192.168.1.20:10000`, `127.0.0.1:10000`. +- `office.zeroq.su` — проксирует на nextcloud onlyoffice. +- `pdf.private` — слушает `0.0.0.0:80`, `100.64.0.0:8446`, `192.168.1.20:8446`, + `127.0.0.1:8446` (для Nextcloud PDF). +- `x.zeroq.su` — 3x-ui controller panel + `/subs/`, `/subsjs/`, `/clash/`. +- `zeroq.su` — корневой, заглушка + `/guest/` → LAN `:80`. +- `vetymae.opencodes.zeroq.su` → `100.86.62.4:4096`. +- `lamet.opencodes.zeroq.su` → `100.106.21.39:6061` — **порт miniflux**; либо + ошибка, либо так задумано [?] (см. вопрос 8.2). +- `opencode.zeroq.su` → `127.0.0.1:4096` (opencode-web на самом sapпира). +- `nextcloud.zeroq.su` → `192.168.1.20:10000`, `/whiteboard` → `:3002`. + +`networking.firewall.allowedTCPPorts = [80 443]` (строка 225) — **мёртвое** правило +при `firewall.enable = false`. + +--- + +## Неотвеченные вопросы + +Слои 9–11 (home-manager, deploy, формат) оставлены для прочтения в +`docs/arch/invariants.md`. Неотвеченные вопросы слоёв 1–8: + +| ID | Вопрос | +|---|---| +| 2.2 | `vetymae` / `lamet` / `therima` / `soptur` — те же машины или хосты вне репозитория? | +| 2.5 | `stateVersion` дрейфует 24.05 / 24.11 / 25.05 / 26.05 — намеренно? | +| 2.6 | Есть ли escape hatch для per-host отличий в `xlib`? | +| 3.2 | `any.nix` (minimal) действительно нуждается в home-manager + sops + disko? | +| 4.1 | Как root получает доступ по SSH — `authorizedKeys` для root в коде нет | +| 4.2 | Как разрешается цикл «ключ в секрете, а нужен для расшифровки»? | +| 4.3 | Все файлы в `secrets/` покрыты `path_regex`? | +| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе? | +| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя»? | +| 5.1 | `/mnt/services` в режиме 0777 — осознанно? | +| 5.3 | NFS выключен, Samba работает — миграция? | +| 5.4 | NTFS-том `lamet-drive` с `mask = "0000"` — что на нём лежит? | +| 5.5 | `therima` / `vetymae` / `soptur` — несуществующие остатки или сетевые шары? | +| 5.6 | Где бэкапы БД и 3x-ui? | +| 6.6 | `192.168.1.20` зашит в 30 мест — считаем константой? | +| 6.7 | DNS дублирует инвентарь сервисов — как проверяем рассинхрон? | +| 6.8 | Публичные IP и SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна? | +| 6.9 | Какой путь REALITY считается правильным? (→ C5) | +| 7.4 | Почему не публиковать весь диапазон 14380-15379? | +| 8.2 | `lamet.opencodes` → `:6061` — ошибка или так задумано? | +| 8.4 | `onlyoffice` — работает после трёх регрессов? | +| 8.5 | Что слушает `:3002` (`/whiteboard` в nextcloud)? | \ No newline at end of file diff --git a/docs/arch/todo.md b/docs/arch/todo.md new file mode 100644 index 0000000..af9a5c3 --- /dev/null +++ b/docs/arch/todo.md @@ -0,0 +1,316 @@ +# TODO: правки и инварианты + +Источник: `docs/arch/invariants.md`. Ответы владельца от 2026-10-05 учтены. +Подтверждённые факты зафиксированы в `AGENTS.md` (корень) и `docs/arch/map.md`; +этот файл — только **незакрытые правки и неотвеченные вопросы**. +Порядок: A → B → C → D, потом E (документация для агента). + +Обозначения: `[ ]` не начато, `[x]` сделано, `[!]` блокирует остальное. + +--- + +## Подтверждено (зафиксировано в `AGENTS.md` / `map.md`) + +Эти инварианты уже учтены в ядре и карте — при правке кода опираться на +зафиксированные формулировки. + +- **6.1** Явная финальная политика nftables на VDS → `todo A3` ещё открыто, + но сам «надо запилить» закреплён. +- **6.3** Firewall на sapphira выключен намеренно (граница — роутер, 5 портов: + 22, 80, 443, 8443, 22000) → формулировка в `AGENTS.md §3`, `todo D1`. +- **6.5** `100.64.0.0` = Tailscale-адрес sapphira (назначен вручную) → `AGENTS.md §5`, + `map.md §Сеть и firewall`. +- **7.1 / 7.2** 3x-ui заморожен: панель на latest, ядро Xray на 26.7.x, + миграция 26.9 провалена → `AGENTS.md §4`, `todo C1–C5`. + +--- + +## Ловушки для агента: выглядит сломанным, но это намеренно + +Прежде чем чинить — проверить этот список. Здесь лежат решения, которые +иначе «поправляются» обратно и ломают рабочую систему. + +| Где | Что выглядит ошибкой | На самом деле | +|---|---|---| +| `configurations/server.nix:130` | `networking.firewall.enable = false` на сервере с 20 сервисами на `0.0.0.0` | Намеренно: фильтр на роутере, он пробрасывает 5 портов (см. D1) | +| `configurations/mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 против 26.05 у остальных | Каждый хост зафиксирован на своей версии; не «подровнять» | +| `modules/users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` с пометкой TODO | Осознанный костыль под старый uid 1000 = `yuyus`; удалять только после миграции ФС | +| `modules/containers/3x-ui.nix:54` | `image = …:latest` | Панель намеренно на последней версии; **ядро** Xray — на 26.7.x, миграция на 26.9 провалена | +| `modules/containers/3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS при откате nginx-stream | Следствие отката `c8d4a12`; смысл утрачен, но опция объявлена — см. C5 | +| `modules/server/default.nix:33-47` | 15 закомментированных модулей с живым кодом | Отключены осознанно; см. E3 | +| `modules/server/{mealie,memos,n8n,netdata,nfs,open-webui,rsync,step-ca,transmission,trilium,zerotier}.nix` | Агент насчитает лишние порты и каталоги | Модули вне `imports` = мёртвый код | +| `home/modules/opencode.nix:339` | `systemd.user.services.opencode-web.Service` вместо привычного `serviceConfig` | `serviceConfig` рендерится в секцию `[serviceConfig]`, которую systemd **молча игнорирует** (`c73a698`) | +| `configurations/vds.nix:73-91` | nftables без финального правила | Известный пробел,см. A3 — **не** «случайно потерялось» | +| `100.64.0.0` в `nginx.nix`, `nextcloud.nix`, `vds/*` | Первый адрес CGNAT `/10`, похож на сетевой | Tailscale-адрес sapphira, назначен вручную | +| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. B1 | + +--- + +## A. Блокеры: сломано или не защищено + +### [ ] A1. `mobile.nix` импортирует несуществующий файл + +**Где:** `configurations/mobile.nix:12` +```nix +xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; }; +``` +Файла `lib/xlib.nix` нет — есть каталог `lib/xlib/` с `default.nix`. +**Правка** (как в `configurations/default.nix:5`): +```nix +xlib = import ../lib/xlib { inherit lib; }; +``` +**Следствие:** до правки `nixOnDroidConfigurations.epral` и `.default` +не вычисляются. Устройство `epral` мертво. +**Проверка:** +``` +nix eval --raw .#nixOnDroidConfigurations.epral.config.environment.etcBackupExtension # ожидается .bak +``` + +### [ ] A2. Убедиться, что `nix flake check` вообще запускается + +**Где:** нет CI; `checks` в `deploy/default.nix:27-29` покрывают только deploy. +**Сначала проверить**, ловит ли текущий `nix flake check` поломку из A1: +``` +nix flake check +``` +Ожидание, которое надо подтвердить: он **уже падает** на `epral`, то есть +проверка существует, но её не запускали. Если падает — A1 и был бы замечен. +**Проверка после A1:** та же команда должна стать зелёной. +**Затем** (E2) — превратить в привычку: прогонять перед каждым коммитом. + +### [ ] A3. Явная финальная политика nftables на VDS + +**Где:** `configurations/vds.nix:73-91` +**Сначала диагностика на otreca** (без неё править опасно — можно отрезать SSH): +``` +nft list ruleset +systemctl status nftables firewall-nftables +``` +Нужно понять, кто реально владеет набором правил: `nftables.enable = true` с +собственным ruleset **и** `networking.firewall.*` включены одновременно +(инвариант 6.2). Затем — править **один** механизм, не оба. +**Что должно получиться** (политика — на выбор владельца, два варианта): +``` +# Вариант «белый список» (предпочтительно): +chain input { + type filter hook input priority 0; policy drop; + iif lo accept + ct state established,related accept + iif "tailscale0" accept + tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept + tcp dport { 22 } ct state new accept # только если 22 нужен на ens3 +} +# Вариант «мягкий» (минимум изменений, фиксирует текущее поведение): +chain input { + type filter hook input priority 0; + iif lo accept + ct state established,related accept + tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept + tcp dport { 80, 443 } ct state new drop + # финал accept — но ТОЛЬКО как явно помеченное «разрешено всё остальное»: + iif "ens3" accept comment "PROVISIONAL: explicit allow-all, см. A3" +} +``` +**Инвариант к записи:** последнее правило самописной цепочки всегда явное. +**Проверка:** `nft list chain inet filter input` + `ssh` с внешнего адреса. + +--- + +## B. Защита данных + +### [ ] B1. Guard на несмонтированный носитель `/mnt/services` + +**Где:** `lib/xlib/helpers.nix` (`mkServiceStorage`), потребители — +`modules/server/{postgresql,n8n,samba,homebox,minecraft}.nix` + `modules/containers/3x-ui.nix` +**Проблема (подтверждена владельцем как не продуманная):** `mkServiceStorage` +даёт `bind,x-systemd.automount,nofail`. Если диск `External` (`xlib.dirs.server-home`, +ext4 по UUID, `configurations/server.nix:55-58`) не смонтирован, то `/mnt/services` +— обычный каталог, `/var/lib/` пуст, и сервис **молча** стартует на чистой +базе. Пользователь увидит «потерялись данные». +**Решение (рекомендую):** добавить в `xlib/helpers.nix` +```nix +mkStorageGuard = + { dir }: + { + # сервис не стартует, пока /mnt/services не смонтирован: + # Requires+After на mnt-services.mount, который упадёт, если нет источника + requiresMountsFor = [ dir ]; + }; +``` +и в каждом потребителе: +```nix +systemd.services.postgresql = xlib.helpers.mkStorageGuard { dir = xlib.dirs.services-mnt-folder; }; +``` +**Важно — не проверять `ConditionPathIsMountPoint=/mnt/services`:** bind-mount +внутри одной ФС не меняет `st_dev`, условие вернёт false даже при корректном +монтировании. Надёжны `requiresMountsFor` или `ConditionPathIsMountPoint` на +`xlib.dirs.server-home` (там `st_dev` действительно другой). +**Плюс операционная строка в `AGENTS.md`:** перед рестартом этих сервисов — +`findmnt /mnt/services`. +**Проверка (имитация отказа):** +``` +systemctl stop postgresql +sudo umount /mnt/services # или остановить automount +systemctl start postgresql # ожидается FAIL, а не пустая база +``` + +### [ ] B2. Зафиксировать, что бэкапов в конфигурации нет + +**Где:** `modules/server/postgresql.nix:23` (`postgresqlBackup.enable` закомментирован), +бэкап-сервиса в репозитории нет вообще; БД 3x-ui — sqlite на том же диске. +**Задача — не код, а запись:** в `AGENTS.md` и `invariants.md` явно сказать, +что бэкапы ведутся вне Nix. Иначе агент считает конфиг самодостаточным. +**Ждёт ответа:** где бэкапы и как их проверять (инвариант 5.6). + +--- + +## C. 3x-ui: заморозить рабочее состояние + +### [ ] C1. Вернуть расследование, потерянное при откате + +**Где:** 200 строк удалены коммитом `22a19be`. +**Восстановить и дополнить выводом:** +``` +git show 9974784:modules/containers/3x-ui-migration-notes.md > docs/arch/notes/3x-ui-xray-26.9.md +``` +Дописать в конец: вердикт — миграция ядра 26.7 → 26.9 **провалена**, откат на +рабочее состояние (панель последняя, ядро 26.7.x), обходные скрипты отключены +осознанно; причина отказа — обязательный постквантовый обмен X25519MLKEM768, +ломающий старых клиентов. +**Инвариант:** откат кода не удаляет расследование; заметка живёт в +`docs/arch/notes/`, а не рядом с откатываемым файлом. + +### [ ] C2. Зафиксировать фактические версии панели и ядра + +**Где:** `modules/containers/3x-ui.nix:54` +**Сначала узнать, что реально работает** (на sapphira и на otreca): +``` +podman images --format '{{.Repository}}:{{.Tag}} {{.Id}} {{.Created}}' | grep 3x-ui +podman inspect ghcr.io/mhsanaei/3x-ui --format '{{index .RepoDigests 0}}' +podman exec 3xui_app /app/bin/xray-linux-amd64 version +``` +**Потом** заменить `:latest` на найденный тег (или digest) в коде. +**Инвариант:** образы контейнеров запинены; `latest` запрещён — обновление +образа это правка в коде, а не `podman pull` на хосте. +**Почему срочно:** `podman.autoPrune.flags = ["--all"]` (`3x-ui.nix:45-47`) + +`:latest` = рабочее состояние может смениться без единого коммита. + +### [ ] C3. Убрать сервис автообновления 3x-ui + +**Где:** `modules/containers/3x-ui.nix:80-90` (`podman-update-3xui_app` с +`podman pull … :latest`) и закомментированный таймер (строка 97-103). +**Предложение:** удалить сервис целиком, оставив комментарий-предупреждение. +Обновление панели через `pull` — ровно тот путь, которым в 2026-10-04 +декларация разошлась с рантаймом; автоматизировать его нельзя. +**Инвариант:** ни один контейнер в этом репозитории не обновляется сам. + +### [ ] C4. Записать в AGENTS.md, что ядро Xray — состояние панели, а не Nix + +Версия ядра выбирается в UI панели и лежит в её sqlite-БД, то есть **вне** Nix. +Репозиторий не может её гарантировать. +**Операционное правило:** перед деплоем/рестартом 3x-ui проверять версию ядра +в панели; обновление ядра = отдельная задача с записью в +`docs/arch/notes/`, а не молчаливый `podman pull`. + +### [ ] C5. Решить судьбу `reality443Forwarding` + +**Где:** `modules/vds/default.nix:19` (`= true`), `modules/options.nix:66-75`, +`modules/containers/3x-ui.nix:33-35`. +Состояние после отката `c8d4a12`: опция включена, поэтому на otreca +пробрасывается `127.0.0.1:15380:443`, тогда как единственный Reality-инбаунд +контейнера слушает 8443, а публичный 8443 проброшен напрямую (`0.0.0.0:8443`). +Потребителя потока (nginx-stream) откат убрал. +**Варианты:** (а) оставить как есть и описать в инвариантах; (б) погасить опцию +в `vds/default.nix` и убрать её из `options.nix`; (в) довести до рабочего +состояния. **Ждёт решения** — связано с 6.9. + +--- + +## D. Сетевая граница: записать то, чего нет в репозитории + +### [ ] D1. Пробросы роутера — главный недостающий инвариант + +Ответ владельца: на сервер пробрасываются **443, 80, 22000 (syncthing), +8443 (xray), 22 (ssh)**. Это **настоящая граница доверия**, и она живёт +в конфиге роутера, то есть вне репозитория. +**Записать в двух местах:** `docs/arch/invariants.md` (слой 6) и `AGENTS.md`. +Формулировка инварианта: +> Экспозиция наружу определяется пробросами на роутере, не `openFirewall`. +> На `sapphira` `networking.firewall.enable = false` намеренно. +> Список пробросов: 22, 80, 443, 8443 (3x-ui/Xray REALITY), 22000 (syncthing). +> Новый сервис не становится доступен из интернета, пока не добавлен проброс. +> `networking.firewall.*` на `sapphira` не имеет эффекта. + +### [ ] D2. Зафиксировать `100.64.0.0` как Tailscale-адрес sapphira + +Моё прежнее замечание («сеть вместо адреса») было неверным — адрес назначен +вручную. Записать как факт + список из 4 мест, которые придётся править при +смене: `modules/server/nginx.nix`, `modules/server/nextcloud.nix`, +`modules/vds/systemd.nix`, `modules/vds/nginx.nix`. +**Опционально (отложено):** вынести `192.168.1.20` в `xlib.dirs` — сейчас +зашит в ~30 местах в 6 файлах. Не срочно, это рефакторинг. + +### [ ] D3. Убрать мёртвое правило firewall + +**Где:** `modules/server/nginx.nix:225-228` — `allowedTCPPorts = [80 443]` +не действует при `firewall.enable = false` (`server.nix:130`). +Удалить или пометить комментарием «депенит от D1». + +--- + +## E. Документация для агента (после прохода по invariants.md) + +### [ ] E1. Написать `AGENTS.md` в корне +Собирается из подтверждённых инвариантов. Структура: карта хостов → +что где лежит → инварианты (нарушишь = сломает) → ловушки из таблицы выше → +команды проверки. Ожидаемый бюджет — до 150 строк. + +### [ ] E2. Выбрать проверки, которые заменят половину инвариантов +Кандидаты из инварианта 11.2: +1. ни одного `:latest` в образах (grep по `image =`); +2. `nix flake check` зелёный — уже ловит A1; +3. домены в `coredns.nix` ↔ vhost'ы в `nginx.nix` совпадают в обе стороны; +4. для каждого потребителя `mkServiceStorage` каталог существует на `External`; +5. последнее правило самописной nftables-цепочки явное; +6. `listen.addr` — адрес интерфейса, а не сеть; +7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`. +**Ждёт ответа:** какие из них делать, какие — избыточны. + +### [ ] E3. Судьба 15 закомментированных модулей +`modules/server/default.nix:33-47` — `remnawave, coturn, mealie, memos, +minecraft, n8n, netdata, nfs, open-webui, rsync, step-ca, stirling-pdf, +transmission, trilium, zerotier`. Удалить или оставить как референс? +Они мешают агенту насчитывать порты и каталоги, которых нет. + +--- + +## F. Ждут ответа (блокируют E1) + +Индексы в `docs/arch/invariants.md`: + +| № | Вопрос, который блокирует запись инварианта | +|---|---| +| 2.2 | `vetymae` / `lamet` / `therima` / `soptur` — это те же машины или хосты вне репозитория? | +| 2.5 | `stateVersion` дрейфует 24.05 / 24.11 / 25.05 / 26.05 — намеренно? | +| 2.6 | Есть ли escape hatch для per-host отличий в `xlib`, или «у всех хостов одно» — закон? | +| 3.2 | `any.nix` (minimal) действительно нуждается в home-manager + sops + disko? | +| 4.1 | Как root получает доступ по SSH — `authorizedKeys` для root в коде нет | +| 4.2 | Как разрешается цикл «ключ `/etc/ssh/id_ed25519` лежит внутри секрета, а нужен для расшифровки» | +| 4.3 | Что лежит в `secrets/`, все ли файлы покрыты `path_regex` | +| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе | +| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя» | +| 5.1 | `/mnt/services` в режиме 0777 — осознанно? | +| 5.3 | NFS выключен, Samba работает — миграция? | +| 5.4 | NTFS-том `lamet-drive` с `mask = "0000"` — что на нём лежит | +| 5.5 | `therima` / `vetymae` / `soptur` — несуществующие остатки или сетевые шары | +| 5.6 | Где бэкапы БД и 3x-ui (→ B2) | +| 6.6 | `192.168.1.20` зашит в 30 мест — считаем константой? | +| 6.7 | DNS дублирует инвентарь сервисов — как проверяем рассинхрон | +| 6.8 | Публичные IP и SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна? | +| 6.9 | Какой путь REALITY считается правильным (→ C5) | +| 7.4 | Почему не публиковать весь диапазон 14380-15379 | +| 8.2 | `lamet.opencodes` → `:6061` — это miniflux; ошибка или так задумано | +| 8.4 | `onlyoffice` — работает после трёх регрессов? | +| 8.5 | Что слушает `:3002` (`/whiteboard` в nextcloud) | +| 9.2 | Кто создаёт `~/Music` и `~/Storage` при `createDirectories = false` | +| 10.1 | Почему `deploy-rs` не деплоит `atoridu`, `wsl`, `epral` |