126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.6 KiB
| okf | type | visibility | status | updated | links |
|---|---|---|---|---|---|
| 0.1 | session-log | private | active | 2026-07-23 |
2026-07-22/23 — Control-plane: dziura w operator_ui + pierwszy pełny cykl remediacji bez SSH
Zamknięcie wielosesyjnego wątku "czemu mózg nie leczy floty". Dwa dni pracy, zakończone pierwszym w historii systemu pełnym cyklem remediacji: supervisor → approval → executor → node-agent → docker restart → completed.
Wykonane
-
Odkrycie bezpieczeństwa + fix (commit
9a5c160, 2026-07-22). Przy reconie kanału dla remediacji CC odkrył, żeoperator_ui.py(port 18180) nasłuchiwał na0.0.0.0na PUBLICZNYM VPS (135.181.153.108) BEZ ŻADNEJ autoryzacji.curlz zewnątrz do/actionszwracał HTTP 200. Gorzej:do_POSTobsługuje/action/mutate→mutate_action(action_id, target_status), które przenosi akcje między stanami WŁĄCZNIE z"approved"— dowolna osoba z internetu mogła zatwierdzić akcję remediacyjną. Jedyne co chroniło: executor w tamtym momencie nie umiał jeszcze wykonać akcji (brak SSH) — czyli przypadek, nie zabezpieczenie. Fix: dual-bind wg wzorca fleet-prometheus —127.0.0.1:18180:8080+${TAILSCALE_BIND_IP}:18180:8080, nowyservices/control-plane/env.example. Zweryfikowane po deployu:curlna publiczny IP → HTTP 000 (brak odpowiedzi),curlpo Tailscale → HTTP 200. Konsumenty nietknięte: node-agent na VPS (network_mode: host, localhost działa dzięki bindowi 127.0.0.1), materializer PIHA (łączy się po100.95.58.48). FOOTGUN do zapamiętania: gdy brakuje.env,docker composetylko OSTRZEGA i po cichu wraca do bindu0.0.0.0— nie failuje. -
Remediacja bez SSH (commit
2dac154, 2026-07-22). Diagnoza stanu wyjściowego: łańcuch działał aż do wykonania — supervisor generuje akcje (18 pending), UI pokazuje z Approve/Reject, przejście pending→approved→running OK, executor podejmuje i dispatchuje. Padało TYLKO wykonanie: executor robiłssh oskar@{node} docker restart {container}, a w kontenerze NIE MA klienta ssh (fail w 6ms =FileNotFoundError), nie ma klucza, nazwa "piha" nie rozwiązuje się z VPS, a klucz VPS nie był autoryzowany na piha.Decyzja architektoniczna: nie dodawać SSH do executora (kontener na publicznym VPS z powłoką na całą flotę = zły blast radius). Zamiast tego: executor ZLECA, node-agent na docelowym węźle WYKONUJE lokalnie przez swój
docker.sock. Kierunek PULL — węzeł sam sięga po zlecenia, VPS nie ma dostępu do węzłów.Wybrany kanał: rsync-pull istniejącym kluczem agenta (tym samym co
_ship_events_to_vps). Odrzucono HTTP pull — bo właśnie odkryto, że 18180 jest publiczny i bez auth (patrz pkt 1), dokładanie tam kanału zleceń zwiększałoby powierzchnię ataku.Przepływ: supervisor → pending → (operator) approved → executor
_execute_action→ running + zapisactions/dispatch/<node>/<action_id>.json→ agent pull (coCHECK_INTERVAL60s) → walidacja → docker SDK.restart()→ eventaction_result→ rsync do VPS → executor_reconcile_running_actions→ completed/failed. TimeoutACTION_TIMEOUT_SECS(300s) → failed z jasnym powodem.Zabezpieczenia: walidacja
node == self.node_name; whitelist typów ={container_restart}(wszystko inne odrzucone z jawnymaction_result); guard przed restartem samego node-agenta; brak wykonywania dowolnych poleceń z payloadu; idempotencja (ponowne zlecenie = no-op). 183 testy. -
Fix uprawnień na PIHA (ręcznie, 2026-07-23 — NIE w repo). Pierwszy test E2E padł: agent rzucał
[Errno 13] Permission denied: /opt/homelab/actions/dispatchco cykl. Root cause to ZNANY, POWRACAJĄCY (już 4. raz — patrz sekcja "Tech-debt: globalny porządek uid/gid/uprawnień we flocie" wkb/phases/backlog.md) motyw uid/gid na PIHA: oskar ma uid 1004, kontener agenta biega jako uid 1000 (= userpina hoście)./opt/homelab/actionsbyłooskar:oskar drwxr-xr-x(utworzone w maju), podczas gdy DZIAŁAJĄCY wzorzec to/opt/homelab/events=oskar:pi drwxrwsr-x(grupapi, zapis dla grupy, setgid). Fix ręcznie:chgrp -R pi+chmod -R g+w+chmod g+sna katalogach. Executor tymczasem zachował się wzorowo: po 300s timeoutu przeniósł akcję dofailedz czytelnym powodem, nic nie zawisło. -
Pierwszy działający cykl remediacji (2026-07-23) — CEL OSIĄGNIĘTY. Po fixie uprawnień agent od razu podjął zalegle zlecenie z poprzedniego dnia i zrestartował
node_exporter, a przy kolejnych cyklach logował "already processed — skipping (idempotency)" — idempotencja potwierdzona w boju.Czysty test E2E (
action_id test-e2e-b): 12:05:25 Executing → 12:05:25 Dispatchedcontainer_restartdo node-agent on piha → 12:05:56 Action completed. CAŁY CYKL 31 SEKUND. Dowód po drugiej stronie:node_exporterna PIHA uptime spadł z 30 godzin do minut. Restart wykonany na zdalnym węźle BEZ ANI JEDNEGO połączenia SSH z control-plane.
Lessons learned
- Recon pod jedno zadanie odkrył poważniejszy problem niż samo zadanie (publiczny 18180) — kolejność prac trzeba było odwrócić: najpierw zamknąć bramę, potem włączać realne wykonywanie akcji. Do momentu fixu remediacji chroniła nas WYŁĄCZNIE niesprawność executora.
- Ten sam motyw uid/gid (host oskar 1004 vs kontener 1000) ugryzł już
czwarty raz: klucz SSH agenta, docker.sock,
events/, terazactions/. Wzorzec działający = grupa współdzielona + setgid. - Zepsuty JSON w
approved/powodował, że executor próbował go przetworzyć W KÓŁKO co 10s ("Failed to move ... to running: Expecting value") — brak przeniesienia dofailed//rejected/. - Cytowanie w zagnieżdżonych heredoc przez ssh:
'$VAR'wewnątrz pojedynczych cudzysłowów podstawia się LOKALNIE — zepsuło JSON akcji testowej. Bezpieczne: heredoc z<<"JSON"i wartości na sztywno.
Stan
Pierwszy w historii systemu pełny cykl remediacji end-to-end potwierdzony w
produkcji (PIHA), bez SSH z control-plane do węzłów. Publiczna dziura
autoryzacji na operator_ui.py:18180 zamknięta (bind ograniczony do
Tailscale). Otwarte follow-upy — patrz kb/phases/backlog.md (retry-w-nieskończoność
zepsutego JSON, uprawnienia actions/ na innych węzłach, brak twardego checka
.env/TAILSCALE_BIND_IP w deploy-local.sh, brak autoryzacji w
operator_ui.py, zapchana approval queue przez alert_only, brak
crash-loop→container_restart, brak Telegram yes/no dla pending, martwy
homeassistant5 na PIHA, repo-less node-agent na lustro).