homelab-codex-ws/kb/runbooks/kb-site-deploy.md
oskar 003f83d453 feat(kb): skrypt publish.sh dla kb-site + skill przypominajacy o publikacji
Automatyzuje kroki 2-3 z kb/runbooks/kb-site-deploy.md (generacja, kontrola
wyciekow, upload przez helper alpine, smoke test) w jednym fail-closed
skrypcie zamiast recznego klikania. Runbook zaktualizowany, zeby
referencjonowal skrypt, z zachowaniem wyjasnien co/dlaczego dla kazdej bramki.

Dodaje skill kb-publish, ktory przypomina operatorowi o publikacji po
zmianach w kb/**/*.md, ale nigdy sam nie uruchamia skryptu (dotyka prod
przez SSH).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 16:00:34 +02:00

12 KiB

okf type visibility status updated links
0.1 runbook private active 2026-08-05
../services/kb-site.md
./npm-api.md
../phases/okit-cloudflare.md

kb-site — deploy, publikacja treści, ingress (kb-e2a24af3.okit.pl)

Serwis: services/kb-site/ (nginx:alpine na PIHA, host port 8250). Generator: scripts/kb/gen_pages.py. Publiczny adres: https://kb-e2a24af3.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).

  • Token bramki dostępny na węźle, z którego generujesz (SATURN/SOLARIA — nie na PIHA): plik /opt/homelab/config/kb-site/.env z jedną linią ACCESS_TOKEN=<token>, chmod 600. To wyjątek od konwencji z CLAUDE.md: config/<serwis>/ trzyma zwykle sekrety węzła, który serwis uruchamia, a ten token jest potrzebny tam, gdzie serwis się generuje. Kontener nginx na PIHA nadal nie ma żadnej konfiguracji ani sekretów.

    Wartość to ten sam token, co w advanced config proxy hosta w NPM. Żyje wyłącznie w bazie NPM i w tym pliku — nigdy w repo.


1. Deploy serwisu na PIHA

# 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.

docker ps --filter name=kb-site
docker volume ls | grep kb-site        # kb-site_kb-site_content

2-3. Generacja, kontrola wycieków, wgranie treści (SATURN / SOLARIA)

Kroki 2-4 (generacja → --check → upload przez helper alpine → smoke test) robi jeden skrypt:

bash scripts/kb/publish.sh

Zobacz kod (scripts/kb/publish.sh) dla dokładnej kolejności komend. Poniżej zostaje wyjaśnienie co skrypt robi i dlaczego — nie trzeba już klikać kroków ręcznie, ale bramki i uzasadnienia poniżej wciąż obowiązują.

Uruchamiamy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA. Docelowy host PIHA jest parametrem na górze skryptu (zmienna PIHA_HOST, domyślnie piha.local).

Skrypt jest fail-closed na każdym etapie: brak/pusty ACCESS_TOKEN, --check z exit 1, albo 0 wystąpień key= w wygenerowanym index.html — każde z tych zatrzymuje publikację przed dotknięciem PIHA.

Token w linkach

Generator czyta ACCESS_TOKEN ze środowiska i dopisuje ?key=<token> do każdego linku wewnętrznego (karty na spisie, linki między dokumentami, powrót „All documents"). Bez tego bramka NPM odbija 403 przy każdym kliknięciu i działa wyłącznie ręcznie sklejony URL — strona jest wtedy technicznie żywa, ale nie da się po niej chodzić.

Token podajemy zmienną środowiskową, nie flagą --access-token: argument z linii poleceń ląduje w historii shella i jest widoczny w ps dla każdego użytkownika węzła. Flaga istnieje do jednorazowych generacji testowych.

Sanity check tuż po generacji — Token: TAK w wyjściu generatora, a w HTML:

grep -c 'key=' build/kb-site/index.html    # > 0; 0 = build bez tokenu

Generacja bez tokenu nie jest błędem — to tryb lokalnego podglądu (build/ do przejrzenia w przeglądarce, bez publikacji). Nie wolno jej tylko wgrać na PIHA.

Token nie jest sekretem chroniącym treść — jest zaporą przed przypadkowym trafieniem. Po tej zmianie siedzi w treści każdej serwowanej strony, więc jedna zapisana strona wydaje go w całości. To nie zmienia modelu zagrożeń (URL wejściowy zawsze go niósł), ale przesądza, że bramką prywatności jest --check i pole visibility, nie ten token.

Kontrola wycieków

--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 (11 dokumentów public): exit 0 — czysto, 22 trafienia wyciszone whitelistą. Wszystkie wyciszone to 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.

Decyzja operatora 2026-08-04: /opt/homelab jest świadomym wyjątkiem — standardowa ścieżka deploy rootu, ta sama na każdym węźle, nie ujawnia sekretów ani topologii. Wpis siedzi w scripts/kb/check_whitelist.txt razem z uzasadnieniem. Wycisza wyłącznie warianty /opt/homelab/...; /home/..., adresy, porty i tokeny dalej zapalają czerwone.

Każdy kolejny wyjątek podlega tej samej regule: najpierw próba wyczyszczenia źródła, wpis do whitelisty dopiero jako świadoma decyzja, zawsze z komentarzem i datą.

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. publish.sh robi dokładnie to: pakuje build/kb-site do tarballa, scp na PIHA, i podmienia wolumen przez ten sam wzorzec helpera co niżej.

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.

Skrypt kończy smoke testem (ssh $PIHA_HOST curl -s localhost:8250) — jeśli chcesz dodatkowej weryfikacji z samej PIHA:

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-e2a24af3
Content: <publiczny IP łącza domowego>   # 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-e2a24af3.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):

dig +short kb-e2a24af3.okit.pl @1.1.1.1

5. NPM — proxy host + certyfikat

Wszystkie komendy npm_api.pydry-run domyślnie; realna zmiana dopiero z --apply. Puść najpierw bez --apply i przeczytaj payload.

# 5a. host: kb-e2a24af3.okit.pl -> PIHA:8250
python3 scripts/npm/npm_api.py --npm piha create-host \
    --domain kb-e2a24af3.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-e2a24af3.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

set -a; . /opt/homelab/config/kb-site/.env; set +a
BASE=https://kb-e2a24af3.okit.pl

curl -sI "$BASE/?key=$ACCESS_TOKEN" | head -1                     # 200
curl -s  "$BASE/?key=$ACCESS_TOKEN" | grep -c 'class="cards"'     # spis jest
curl -s  "$BASE/subsystems/observer.html?key=$ACCESS_TOKEN" | tail -5   # stopka: data + commit

# bramka faktycznie broni — bez tokenu ma NIE być 200
curl -sI "$BASE/" | head -1

# przejście po linku ze spisu, tak jak zrobi to przeglądarka:
# wyciągnij pierwszy href z indexu i pobierz go bez dopisywania niczego ręcznie
LINK=$(curl -s "$BASE/?key=$ACCESS_TOKEN" \
        | grep -o 'href="[^"]*key=[^"]*"' | head -1 | cut -d'"' -f2)
curl -sI "$BASE/$LINK" | head -1                                  # 200

Ostatni test jest właściwą weryfikacją tej bramki: sprawdza, że link wygenerowany przez generator przechodzi przez NPM, a nie że ręcznie sklejony URL działa.

W przeglądarce: https://kb-e2a24af3.okit.pl/?key=<token> — spis pogrupowany per type, kłódka bez ostrzeżeń, wejście w dowolną kartę kliknięciem (nie przez pasek adresu), powrót linkiem „All documents", i z powrotem w inną kartę.

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 scripts/kb/publish.sh

To ten sam skrypt co w krokach 2-3 — rutynowa aktualizacja to po prostu ponowne uruchomienie całej publikacji. Bramki (ACCESS_TOKEN pusty, --check exit 1, brak key= w spisie) zatrzymują ją przed dotknięciem PIHA przy każdym uruchomieniu, nie tylko przy pierwszym.

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
index otwiera się z tokenem, ale kliknięcie w kartę daje 403 build wygenerowany bez ACCESS_TOKEN — linki nie niosą ?key= ustaw token (krok 0) i powtórz kroki 2-3; grep -c 'key=' build/kb-site/index.html
403 także na URL z tokenem token w NPM został wymieniony i rozjechał się z /opt/homelab/config/kb-site/.env zsynchronizuj plik z advanced config w NPM, przegeneruj i opublikuj — stary build zostaje z martwym tokenem w każdym linku
--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):

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.