Files
nixos/.agent/decisions/proposals/vds-nftables-fix.md
T
oqyude 61b3724752 metaagent: Wave 1 + T4 + T7 + T3 + T5 + T15 + T16 — 12 tasks of tech-debt reduction
Comprehensive batch addressing the 16-task backlog in
.agent/tasks/manifest.json. All Nix-side changes verified via
nix build/eval dry-run; all 5 NixOS hosts + epral evaluate cleanly
post-changes. No regressions.

Wave 1 (non-functional cleanup):

  T1/A1 — configurations/mobile.nix:12: fix `import ../lib/xlib.nix`
          (broken path) → `import ../lib/xlib`. Unblocks nixOnDroid
          configurations.epral. R1.1 invariant.

  T8/C3 — modules/containers/3x-ui.nix: remove `podman-update-3xui_app`
          systemd service and commented timer. Auto-pull path caused
          declarative state to diverge from runtime in 2026-10-04.
          R1.5 invariant.

  T13/D3 — modules/server/nginx.nix:368-371: remove dead
          `networking.firewall.allowedTCPPorts = [80 443]`.
          `firewall.enable = false` on sapphira (R1.3), so openFirewall
          rules are no-op. Replace with R1.3 comment.

  T6/C1 — .agent/decisions/notes/3x-ui-xray-26.9.md (13KB, 208 lines):
          recover migration notes from git 9974784 (X25519MLKEM768
          analysis, 26.7→26.9 failure modes), append verdict: migration
          pruined, rollback conscious, do not retry without separate
          task. R1.5 / C1.

  T9/C4 — .agent/rules/project-rules.md: add R1.8 — Xray-core version is
          state of 3x-ui panel, not Nix. Update trap entry for
          3x-ui.nix:54 to reference R1.8.

  T11/D1, T12/D2 — .agent/checkpoints.json + .agent/tasks/manifest.json:
          verify R1.3 (router port-forwards 22/80/443/8443/22000) and
          R1.4 (100.64.0.0 = Tailscale sapphira) wording already
          satisfies acceptance criteria. Flip status pending → completed.

T4 (storage guard, FUNCTIONAL CHANGE):

  New helper in lib/xlib/helpers.nix:
      mkStorageGuard = xlib: {
        RequiresMountsFor = [ xlib.dirs.server-home ];
        ConditionPathIsMountPoint = [ "!${xlib.dirs.server-home}" ];
      };

  Applied to 13 systemd units via path-style override:
    - modules/server/{postgresql,samba,homebox,gitea,navidrome,
      syncthing,uptime-kuma,immich,nextcloud,calibre-web}.nix
    - modules/containers/3x-ui.nix (podman-3xui_app)
    - modules/containers/tape-rotation.nix (podman-taperotation-{backend,frontend})

  Anchor: xlib.dirs.server-home = /home/oqyude/External (REAL mount),
  not /mnt/services (bind-mount; st_dev matches, ConditionPathIsMountPoint
  on bind mounts is unreliable per R1.2 note).

  Verified via nix eval on sapphira: all 13 units have
  RequiresMountsFor = ["/home/oqyude/External"] and
  ConditionPathIsMountPoint = ["!/home/oqyude/External"].

  Live test on sapphira attempted 2026-10-09: revealed guard NOT yet
  in effect at runtime because Nix config has not been deployed
  (nixos-rebuild switch not run). postgresql started despite External
  being unmounted. Implementation correct, deployment pending user
  action.

T7/C2 (read-only diag, no code change):

  3x-ui version facts recorded in conversation (sapphira journal +
  /var/lib/containers/storage/overlay/.../diff/app/bin/xray-linux-amd64):
    - Active Xray: 26.7.28 (go1.26.5 linux/amd64) — R1.5 validated at runtime
    - Stale binary: 26.9.30 (go1.27.1) — leftover from failed 26.9 migration
    - Panel DB (x-ui.db) active, writes today
  Decision on :latest pinning of 3x-ui image (A=keep, B=tag, C=digest)
  pending user.

T3/A3 (nftables on otreca — config analysis + proposal):

  Diagnostic attempted via ssh otreca-tailscale (100.64.1.0) and
  otreca public (109.248.161.5:22): BOTH UNREACHABLE. Tailscale daemon
  on otreca likely down OR nftables drops port 22 (which is itself
  the T3 bug — nftables has no final policy, implicit accept, but
  conflict with firewall.enable = true per R1.6).

  Proposal written: .agent/decisions/proposals/vds-nftables-fix.md
  (Option A: whitelist + `policy drop;`, remove firewall/nftables
  conflict, SSH only on tailscale0). Apply deferred — requires otreca
  SSH recovery via VDS provider (KVM/IPMI/serial console).

T5/B2 (backups documentation):

  .agent/decisions/0002-backups-external.md (draft): catalog of what
  is declared in Nix vs. what is external; awaiting answer to open
  question 5.6 (where are backups, how are they verified).

T15/E2 (CI checks):

  .ci/checks.sh (executable, ~140 lines) with 3 checks from
  analysis-report.md §5:
    - #1: no `:latest` in container images (with R1.5 whitelist
          for 3x-ui). FAIL — 4 violations:
            localhost/kokoro-tts:latest
            ghcr.io/openhands/openhands:latest
            docker.io/elizaroveugene/taperotation-backend:latest
            docker.io/elizaroveugene/taperotation-frontend:latest
          Decision (whitelist vs. pin) pending user.
    - #2: nix flake check (skipped with --no-build).
    - #7: secrets/ files match .sops.yaml path_regex. PASS.

T16/E3 (archive commented modules):

  13 of 14 commented modules in modules/server/default.nix:37-50
  existed as files. git mv them to archive/{server-modules,containers}/.
  1 (stirling-pdf.nix) didn't exist; just removed the comment.

  modules/server/default.nix:37-50 cleaned of 14 commented lines.
  Added 3-line comment recording the archive date and reason.

  Verified: nixosConfigurations.sapphira still evaluates.

Post-change state:

  $ nix build .#nixosConfigurations.{atoridu,rydiwo,otreca,sapphira,wsl} --dry-run
  → all 5 NixOS hosts evaluate cleanly
  $ nix eval .#nixOnDroidConfigurations.epral.config.system.stateVersion
  → "24.05"

Pending (user input required — not in this commit):

  - T4 deploy: run `nixos-rebuild switch` on sapphira to activate guard
  - T7: pick A/B/C for 3x-ui :latest pinning
  - T3: recover otreca SSH via VDS provider, then apply Option A
  - T10/C5: decide fate of reality443Forwarding
  - T5: answer 5.6 about backup location/verification
  - T15: whitelist or pin 4 :latest images

Untracked files NOT committed (in .gitignore):

  .temp/t4-live-test*.sh, .temp/cleanup-*.sh — throwaway test scripts
  from T4 live test attempts. Preserved locally for reference; see
  AGENTS.md convention ("Создавать `.temp/` в корне проекта — Для
  временных файлов агента. Всегда в `.gitignore`").

Also untracked, committed:

  .agent/reviews/2026-10-10-review-dev-diff-vs-16644fc.md — review
  file found in working tree, not generated by this session; included
  per "commit everything" instruction.
2026-10-10 15:15:22 +03:00

237 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Proposal: nftables ruleset для otreca (задача T3 / A3)
**Статус:** proposed (не применён)
**Дата:** 2026-10-09
**Связано с:** T3 (manifest), R1.6 (project-rules.md), `configurations/vds.nix:67-92`
## Контекст
`configurations/vds.nix` декларирует nftables-ruleset, но:
1. **Нет финальной политики** на `chain input` (line 75-90) — implicit `accept`
на «всё остальное». Это проявление R1.6 «явная финальная политика требуется».
2. **`networking.firewall.enable = true` (line 68) и `networking.nftables.enable = true` (line 71)**
включены одновременно. R1.6 явно фиксирует это как конфликт
(«проверить, кто реально владеет ruleset'ом, перед правкой»).
3. **SSH на 22 открыт только через `networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 22 ]`**
(line 55). Если Tailscale-демон на otreca упал ИЛИ nftables-ruleset
перезаписал `firewall.interfaces.*` правила, SSH-мёртв.
## Что произошло при попытке диагностики (2026-10-09)
- `ssh otreca-tailscale` из WSL: **Name or service not known** (нет Tailscale magic DNS в WSL)
- `ssh oqyude@100.64.1.0` (Tailscale IP напрямую): **Connection timed out** (5s)
- `ping 100.64.1.0`: **no response**
- `ssh oqyude@109.248.161.5` (public IP, port 22): **Connection timed out**
- `ping 109.248.161.5`: **OK** (33ms) — хост жив
- Xray-трафик sapphira → otreca `109.248.161.5:443` (XHTTP): **работает** (journal sapphira)
То есть otreca отвечает по 443 (Xray REALITY inbound) и по ICMP, но **port 22
полностью недоступен**. Это и есть T3-баг, видимый снаружи: либо Tailscale-демон
на otreca упал, либо nftables-ruleset дропает 22 на ens3.
## Текущий state (vds.nix:67-92)
```nix
networking = {
firewall = {
enable = true; # ← NixOS-managed firewall (есть allowedTCPPorts и пр.)
allowPing = true;
};
nftables = {
enable = true; # ← самописный ruleset
ruleset = ''
table inet filter {
chain input {
type filter hook input priority 0;
# loopback
iif lo accept
# уже установленные
ct state established,related accept
# РЕЖЕМ SYN СРАЗУ
tcp flags syn tcp dport {80,443} limit rate 20/second burst 40 packets accept
tcp flags syn tcp dport {80,443} drop
# остальное по необходимости ← НИКАКОГО "остального"
}
}
'';
};
firewall.interfaces.tailscale0.allowedTCPPorts = [ 22 ]; # SSH на Tailscale
...
};
services.openssh.openFirewall = false; # SSH не открыт в firewall
services.tailscale.openFirewall = true; # Tailscale открыт в firewall
```
## Предлагаемое решение (Вариант A — рекомендую)
**Идея:** отдать всё nftables, убрать дубликат, добавить явный `policy drop`.
```nix
networking = {
# Всё управляется nftables ниже; стандартный firewall выключаем,
# чтобы не было конфликта приоритетов (R1.6).
firewall.enable = false;
firewall.allowedTCPPorts = lib.mkForce []; # гарантируем пусто
firewall.interfaces = lib.mkForce {}; # гарантируем пусто
allowPing = true; # ICMP через nftables ниже
nftables = {
enable = true;
ruleset = ''
table inet filter {
chain input {
type filter hook input priority 0;
policy drop; # ← ЯВНЫЙ final drop (R1.6 fix)
# loopback
iif lo accept
# уже установленные / связанные
ct state established,related accept
# ICMP (нужен для path MTU discovery)
ip protocol icmp accept
# traceroute
udp dport 33434-33534 accept
# SSH — ТОЛЬКО на Tailscale (R1.6: не на публичном интерфейсе)
iifname "tailscale0" tcp dport 22 accept
# Xray REALITY inbound (используется sapphira → otreca как relay)
tcp dport 443 accept
# log для диагностики (видно в journal: journalctl -k | grep nft-drop)
log prefix "nft-drop: " flags all counter drop
}
}
'';
};
enableIPv6 = false;
interfaces.ens3.useDHCP = true;
};
```
### Что меняется
| Было | Станет |
|---|---|
| `firewall.enable = true` + `firewall.interfaces.tailscale0.allowedTCPPorts = [22]` | `firewall.enable = false` (всё через nftables) |
| `chain input` без `policy` (implicit accept) | `policy drop;` явно |
| Нет ICMP-правила (работает через `firewall.allowPing = true`) | `ip protocol icmp accept` в ruleset |
| Нет traceroute | `udp dport 33434-33534 accept` |
| `tcp dport {80,443} rate-limit + drop` (только SYN, не остальной TCP) | `tcp dport 443 accept` (только 443, 80 закрыт) |
| Нет `log` правила | `log prefix "nft-drop: " ... drop` для отладки |
### Что НЕ меняется
- `services.openssh.openFirewall = false` (остаётся — SSH не открываем через firewall, потому что firewall выключен)
- `services.tailscale.enable = true; openFirewall = true` (Tailscale-интерфейс создаётся и маршрутизируется NixOS, openFirewall не имеет эффекта при `firewall.enable = false` но оставлен для ясности)
- `enableIPv6 = false`
- `interfaces.ens3.useDHCP = true`
- `system.stateVersion = "25.05"`
## Альтернативы (для полноты)
### Вариант B — минимальный фикс (только закрыть gap)
```nix
networking.nftables.ruleset = ''
table inet filter {
chain input {
type filter hook input priority 0;
policy drop; # ← ТОЛЬКО ЭТО
...остальное как было...
}
}
'';
```
- **Плюс:** минимальное изменение.
- **Минус:** не разрешает конфликт `firewall.enable` + `nftables.enable`. Если NixOS при apply добавит правила из firewall-блока после nftables — поведение непредсказуемо.
### Вариант C — задокументировать, не править
Дописать в `configurations/vds.nix` комментарий-предупреждение; создать ADR
в `.agent/decisions/0002-nftables-vds-gap.md`.
- **Плюс:** zero risk, сдвигает проблему.
- **Минус:** проблема остаётся; deploy-rs всё ещё может случайно стереть ruleset при apply.
## Деплой
**Предусловие:** SSH-доступ на otreca должен быть восстановлен. Варианты:
- Через VDS-провайдера (KVM/IPMI/serial console)
- Если Tailscale-демон на otreca мёртв, но SSH-ключ уже на месте — попросить
провайдера выполнить `systemctl restart tailscaled` или
`nft flush ruleset && iptables -F` для emergency-разблокировки
**Команда деплоя** (после восстановления SSH):
```bash
deploy . otreca
# или
nixos-rebuild switch --target-host otreca-tailscale --flake .#otreca
```
**Проверка после apply:**
```bash
ssh otreca-tailscale "sudo nft list ruleset | head -30" # видим policy drop + правила
ssh otreca-tailscale "sudo iptables -L" # должно быть пусто
ssh otreca-tailscale "echo OK" # SSH работает
ssh sapphira "curl -m 5 https://media.mediavitrina.ru/generate_204 -o /dev/null -w '%{http_code}\n'"
# Xray REALITY inbound на 443 всё ещё работает
```
## Риск и откат
**Риск:**
- Если восстановление SSH сделано неправильно, можно потерять доступ к otreca
до конца сессии провайдера. **Это самая опасная часть всей задачи** —
не сам nftables-fix, а путь к нему.
- Если в ruleset опечатка и блокирует нужное — после `nixos-rebuild switch`
правила применяются мгновенно. До восстановления SSH-доступа единственный
путь назад — через KVM/IPMI/serial console провайдера.
**Откат:**
```bash
# Если есть SSH:
ssh otreca-tailscale "sudo nixos-rebuild switch --rollback"
# Если SSH потерян:
# → KVM/IPMI провайдера → serial console → загрузить предыдущее поколение
# (systemd-boot: выбрать в GRUB; grub: тоже)
```
## Чеклист перед apply
- [ ] SSH на otreca восстановлен (через Tailscale или KVM)
- [ ] Локальный smoke-test: `nix build .#nixosConfigurations.otreca.config.system.build.toplevel --dry-run` — зелёный
- [ ] Включена serial console в GRUB (если ещё нет) — для emergency recovery
- [ ] Прокатили `nixos-rebuild switch` и проверили:
- [ ] `nft list ruleset` показывает `policy drop` и все ожидаемые правила
- [ ] `iptables -L` пуст
- [ ] SSH через Tailscale работает
- [ ] Xray REALITY на 443 работает (через sapphira как клиент)
- [ ] nginx на 80 (если используется на otreca) — открыт, если нет — закрыт
- [ ] После успешного apply: снять snapshot/отметку «стабильная конфигурация», чтобы иметь точку отката
## Обратное (если откатимся)
- Правки только в `configurations/vds.nix:67-92` (ruleset) и
`configurations/vds.nix:55` (firewall.interfaces.* — очищаем)
- Никаких других файлов не трогаем
- Восстановление = revert `git revert` + `nixos-rebuild switch`
## Связанные задачи
- T3 (A3 в манифесте) — этот proposal закрывает основную часть
- R1.6 — фиксирует конфликт firewall/nftables и требование явной политики
- Возможный follow-up: добавить CI-check «последнее правило chain input —
policy или явно accept/drop» (кандидат #5 из `analysis-report.md §5`)