From 41d354308240cf490b5fd55c875c3300c12b7c4a Mon Sep 17 00:00:00 2001 From: oskar Date: Tue, 4 Aug 2026 18:01:56 +0200 Subject: [PATCH] =?UTF-8?q?docs(kb):=20kb-site=20=E2=80=94=20dokument=20se?= =?UTF-8?q?rwisu=20(public)=20+=20runbook=20deployu=20(private)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kb/services/kb-site.md (type: service, visibility: public) — opis samej wystawki: co jest publikowane (regula fail-closed na `visibility`), jak renderowane sa odnosniki do dokumentow nieopublikowanych, struktura builda, stopka z commitem, bramka --check. Napisany tak, zeby sam przechodzil --check: zero adresow, portow i sciezek hosta — szczegoly operacyjne siedza w runbooku. kb/runbooks/kb-site-deploy.md (type: runbook, visibility: private) — pelna procedura: deploy na PIHA, generacja + kontrola wyciekow jako bramka publikacji, podmiana tresci w wolumenie helperem alpine (wzorzec z services/narty27/README.md, rozszerzony z pliku na drzewo), rekord A w Cloudflare + Pi-hole local DNS (split-horizon), proxy host i cert przez scripts/npm/npm_api.py, weryfikacja, rutynowa aktualizacja jednym lancuchem &&, tabela rollback/typowe problemy. Dwie rzeczy zapisane wprost, bo latwo je przeoczyc: - kolejnosc DNS -> NPM host -> cert (HTTP-01 wymaga dzialajacego vhosta), - `rm -rf /content/*` przed rozpakowaniem — bez tego dokument przelaczony z public na private zostaje w wolumenie i dalej jest serwowany. Runbook notuje tez aktualny wynik --check (22 trafienia /opt/homelab w dokumentach public) jako decyzje do podjecia przed pierwsza publikacja: wyczyscic zrodla albo swiadomie wpisac wyjatek do whitelisty. --- kb/runbooks/kb-site-deploy.md | 248 ++++++++++++++++++++++++++++++++++ kb/services/kb-site.md | 74 ++++++++++ 2 files changed, 322 insertions(+) create mode 100644 kb/runbooks/kb-site-deploy.md create mode 100644 kb/services/kb-site.md diff --git a/kb/runbooks/kb-site-deploy.md b/kb/runbooks/kb-site-deploy.md new file mode 100644 index 0000000..44cf7eb --- /dev/null +++ b/kb/runbooks/kb-site-deploy.md @@ -0,0 +1,248 @@ +--- +okf: "0.1" +type: runbook +visibility: private +status: active +updated: 2026-08-04 +links: + - ../services/kb-site.md + - ./npm-api.md + - ../phases/okit-cloudflare.md +--- + +# kb-site — deploy, publikacja treści, ingress (kb.okit.pl) + +Serwis: `services/kb-site/` (nginx:alpine na PIHA, host port **8250**). +Generator: `scripts/kb/gen_pages.py`. Publiczny adres: `https://kb.okit.pl`. + +Kolejność ma znaczenie: **DNS → NPM host → cert**. Certyfikat Let's Encrypt +leci challenge'em HTTP-01, więc rekord A musi już wskazywać na łącze domowe, +a vhost musi już odpowiadać na porcie 80, zanim zamówisz cert. + +--- + +## 0. Wymagania wstępne + +- PIHA ma checkout repo w `~/homelab-codex-ws` i działającego Dockera. +- npm@PIHA działa (`http://192.168.31.5:81`), port 80/443 z routera jest + przekierowany na PIHA — tak samo jak dla `vikunja.okit.pl`. +- `scripts/npm/.env` wypełniony poświadczeniami (patrz `kb/runbooks/npm-api.md`). +- Strefa `okit.pl` w Cloudflare (patrz `kb/phases/okit-cloudflare.md`). + +--- + +## 1. Deploy serwisu na PIHA + +```bash +# na PIHA +cd ~/homelab-codex-ws +git pull +docker compose -f services/kb-site/docker-compose.yml up -d +``` + +Bez `.env` i bez override'a — serwis nie ma konfiguracji ani sekretów. + +Kontener wstanie, ale healthcheck będzie **unhealthy do czasu wgrania treści** +(pusty wolumen = brak `index.html`). To normalne między krokiem 1 a 3. + +```bash +docker ps --filter name=kb-site +docker volume ls | grep kb-site # kb-site_kb-site_content +``` + +--- + +## 2. Generacja treści + kontrola wycieków (SATURN / SOLARIA) + +Generujemy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA. + +```bash +python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl +python3 scripts/kb/gen_pages.py --check +echo $? # 0 = czysto, 1 = trafienia +``` + +`--check` skanuje **wygenerowany HTML**, nie źródła: adresy IP (RFC1918, +Tailscale 100.64/10, publiczne v4/v6), porty 1024-65535, ścieżki `/home/` +i `/opt/`, długie hexy i ciągi base64 wyglądające na tokeny. + +**Kontrola jest bramką publikacji — nie kopiuj treści na PIHA przy exit 1.** + +Stan na 2026-08-04 (10 dokumentów public): **22 trafienia, wszystkie +`path-host` — `/opt/homelab/...` w dokumentach opisujących layout runtime** +(`subsystems/observer`, `subsystems/standards`, `subsystems/service-model`, +`subsystems/event-system`, `subsystems/agent-operating-procedures`, +`subsystems/action-approval-model`, `runbooks/node-onboarding`). Zero IP, zero +portów, zero tokenów. + +Przed pierwszą publikacją trzeba to świadomie rozstrzygnąć — dwie drogi: + +- **wyczyścić źródła**: pousuwać konkretne ścieżki z dokumentów `public` + (najbezpieczniejsze, ale te dokumenty w dużej części o tych ścieżkach są), albo +- **wpisać wyjątek**: `/opt/homelab` w `scripts/kb/check_whitelist.txt`. + Ścieżka `/opt/homelab` to konwencja repo, nie sekret; jest opisana w publicznym + `CLAUDE.md`-owym modelu i nie zdradza ani hosta, ani użytkownika. Wpis + wycisza wszystkie warianty `/opt/homelab/...` i **nie** wycisza `/home/...`. + +Cokolwiek wybierzesz — udokumentuj decyzję w whitelist (plik jest po to, żeby +wyjątek był zapisany, nie domyślny). + +--- + +## 3. Wgranie treści do wolumenu (helper alpine) + +Wzorzec z `services/narty27/README.md`, rozszerzony z jednego pliku na drzewo: +`docker cp` nie zapisze do montowania `:ro`, więc treść wjeżdża jednorazowym +kontenerem pomocniczym, który montuje wolumen zapisywalnie. + +```bash +# 1. spakuj build (na węźle generującym) +tar -czf /tmp/kb-site.tgz -C build/kb-site . + +# 2. wyślij na PIHA +scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz + +# 3. podmień zawartość wolumenu przez helper +ssh piha 'docker run --rm \ + -v kb-site_kb-site_content:/content \ + -v /tmp:/src:ro \ + alpine sh -c "rm -rf /content/* /content/.[!.]* 2>/dev/null; \ + tar -xzf /src/kb-site.tgz -C /content"' + +# 4. posprzątaj staging +rm /tmp/kb-site.tgz +ssh piha 'rm /tmp/kb-site.tgz' +``` + +`rm -rf /content/*` przed rozpakowaniem jest **obowiązkowe**: bez tego strona +dokumentu przełączonego z `public` na `private` zostałaby w wolumenie i dalej +serwowała treść, której już nie publikujemy. Publikacja jest podmianą całości, +nie dogrywaniem plików. + +Weryfikacja z PIHA: + +```bash +services/kb-site/healthcheck.sh +curl -sI http://192.168.31.5:8250/index.html | head -1 +``` + +--- + +## 4. Cloudflare — rekord A + +``` +Type: A +Name: kb +Content: # ten sam, co ma vikunja.okit.pl +Proxy: DNS only (szara chmurka) +TTL: Auto +``` + +Docelowy IP weź z istniejącego rekordu `vikunja` w tej samej strefie zamiast +wpisywać z pamięci — łącze domowe potrafi zmienić adres. + +**DNS only, nie proxied**: challenge HTTP-01 w kroku 5 musi trafić na nginx, +a nie na krawędź Cloudflare. Włączenie proxy to osobna decyzja, po tym jak cert +już działa. + +Split-horizon (patrz `kb/phases/okit-cloudflare.md`): w Pi-hole na PIHA dodaj +Local DNS Record `kb.okit.pl → 192.168.31.5`, żeby klienci w LAN szli prosto do +npm, a nie przez hairpin NAT na routerze. + +Sprawdzenie propagacji (z hosta poza LAN albo przez publiczny resolver): + +```bash +dig +short kb.okit.pl @1.1.1.1 +``` + +--- + +## 5. NPM — proxy host + certyfikat + +Wszystkie komendy `npm_api.py` są **dry-run domyślnie**; realna zmiana dopiero +z `--apply`. Puść najpierw bez `--apply` i przeczytaj payload. + +```bash +# 5a. host: kb.okit.pl -> PIHA:8250 +python3 scripts/npm/npm_api.py --npm piha create-host \ + --domain kb.okit.pl \ + --forward-host 192.168.31.5 --forward-port 8250 \ + --block-exploits --http2-support +# ...i to samo z --apply + +# 5b. cert Let's Encrypt (HTTP-01 — wymaga działającego kroku 4 i 5a) +python3 scripts/npm/npm_api.py --npm piha create-cert --domain kb.okit.pl --apply + +# 5c. podepnij cert pod host i wymuś HTTPS +python3 scripts/npm/npm_api.py --npm piha list-hosts # weź HOST_ID +python3 scripts/npm/npm_api.py --npm piha list-certs # weź CERT_ID +python3 scripts/npm/npm_api.py --npm piha set-cert HOST_ID CERT_ID --ssl-forced --apply +``` + +Jeśli w npm@PIHA istnieje już wildcard `*.okit.pl` (faza 1 z +`kb/phases/okit-cloudflare.md`), pomiń 5b i w 5c podepnij jego `CERT_ID` — +jeden cert DNS-01 zamiast kolejnego per-host HTTP-01. + +Diagnostyka nieudanego certu: `ssh piha 'docker logs npm --tail 100'` — NPM +zwraca wyjście certbota w treści błędu, a przy porażce challenge'u kasuje +wiersz certyfikatu (szczegóły w `kb/runbooks/npm-api.md`). + +--- + +## 6. Weryfikacja końcowa + +```bash +curl -sI https://kb.okit.pl/ | head -1 # 200 +curl -s https://kb.okit.pl/ | grep -c 'class="cards"' # spis jest +curl -s https://kb.okit.pl/subsystems/observer.html | tail -5 # stopka: data + commit +``` + +W przeglądarce: `https://kb.okit.pl` — spis pogrupowany per type, kłódka bez +ostrzeżeń, wejście w dowolną kartę, powrót linkiem „All documents". + +Kontrola treści (ręczna, jednorazowa po pierwszej publikacji): przejrzyj spis +i potwierdź, że nie ma tam nic, czego nie chcesz mieć w internecie. Generator +pilnuje pola `visibility`, ale to człowiek decyduje, co dostaje `public`. + +--- + +## 7. Rutynowa aktualizacja treści + +Po każdej zmianie w `kb/**/*.md`, która dotyczy dokumentów `public`: + +```bash +python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl \ + && python3 scripts/kb/gen_pages.py --check \ + && tar -czf /tmp/kb-site.tgz -C build/kb-site . \ + && scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz \ + && ssh piha 'docker run --rm -v kb-site_kb-site_content:/content -v /tmp:/src:ro \ + alpine sh -c "rm -rf /content/* /content/.[!.]* 2>/dev/null; tar -xzf /src/kb-site.tgz -C /content"' \ + && ssh piha 'rm /tmp/kb-site.tgz' \ + && rm /tmp/kb-site.tgz +``` + +Łańcuch na `&&` jest celowy: `--check` z exit 1 zatrzymuje publikację. + +Kontener nie wymaga restartu — nginx czyta wolumen na bieżąco. + +--- + +## 8. Rollback i typowe problemy + +| Objaw | Przyczyna | Reakcja | +|---|---|---| +| healthcheck unhealthy, 404 na `/` | pusty wolumen — treść nigdy nie wjechała | krok 3 | +| stara strona po publikacji | pominięte `rm -rf /content/*` | powtórz krok 3 w całości | +| dokument nie pojawia się na stronie | brak `visibility: public` albo niepoprawny frontmatter (fail-closed) | `python3 scripts/kb/check_okf.py`, potem regeneracja | +| link renderuje się jako tekst z `[private]` | cel jest prywatny albo nie istnieje | tak ma być — to nie błąd | +| `--check` zapala nowe trafienie | do dokumentu `public` wjechał adres/ścieżka/token | popraw źródło; whitelist tylko świadomie | +| cert nie schodzi | rekord A nie propagował, proxy Cloudflare włączone, albo port 80 niedostępny | krok 4, potem 5b ponownie | + +Awaryjne wygaszenie wystawki (bez ruszania NPM i DNS): + +```bash +ssh piha 'cd ~/homelab-codex-ws && docker compose -f services/kb-site/docker-compose.yml down' +``` + +Wolumen zostaje; `up -d` przywraca stan sprzed. Pełne odtworzenie od zera to +kroki 1-3 — treść jest artefaktem repo, nie danymi. diff --git a/kb/services/kb-site.md b/kb/services/kb-site.md new file mode 100644 index 0000000..5de6e79 --- /dev/null +++ b/kb/services/kb-site.md @@ -0,0 +1,74 @@ +--- +okf: "0.1" +type: service +visibility: public +status: active +updated: 2026-08-04 +links: + - ../runbooks/kb-site-deploy.md +--- + +# kb-site + +Public slice of this knowledge base, served as static HTML at `kb.okit.pl`. +Plain `nginx:alpine` on the PIHA node reading one Docker named volume — no +build step at runtime, no database, no dependencies. + +The site is a rendering, not a source. Every page is generated from the +Markdown documents of the knowledge base by `scripts/kb/gen_pages.py` and +copied into the volume; the HTML is never edited by hand and never committed. + +## What gets published + +Only documents that carry an explicit `visibility: public` field in their +frontmatter. The generator is **fail-closed**: a document with no frontmatter, +with unparseable frontmatter, with no `visibility` field, or with any other +value is treated as private and stays out of the build. + +The same rule applies to cross-references. A link pointing at a document that +was not published is not rendered as a link — only the label survives, marked +`[private]`. A public page therefore never exposes the location of an internal +document and never produces a dead link. + +Frontmatter shown on a page is deliberately partial: type, status and the last +update date. The `links` field is omitted, because a path to a private document +is already a leak of its name. + +## Structure of the build + +``` +build/kb-site/ + index.html list of all published documents, grouped by type + /.html one page per document, mirroring the source tree +``` + +Each page carries a footer with the generation timestamp and the short commit +hash of the repository state it was rendered from, so any page can be traced +back to an exact revision. + +## Leak gate + +`gen_pages.py --check` re-reads the generated HTML — not the sources — and +fails on anything that looks like infrastructure detail leaking into a public +page: private, carrier-grade and public IP addresses (v4 and v6), high service +port numbers, absolute host paths, and long hex or base64 strings that look +like credentials. Deliberate exceptions live in a whitelist file that starts +out empty, so every exception is a recorded decision. + +The check is a release gate: content is copied to the host only after it +passes. + +## Operations + +Deployment, content refresh, reverse-proxy and DNS setup are described in the +[kb-site deployment runbook](../runbooks/kb-site-deploy.md), which is internal — +on this site the reference above is plain text, exactly as described in the +previous section. + +## Content lifetime + +The volume holds an artifact, not data. There is no backup job — recovery is a +regeneration from the repository. Because the generator writes a fresh tree on +every run and the publish step replaces the volume contents wholesale, a +document that flips from public to private disappears from the site on the next +publish.