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>
12 KiB
| okf | type | visibility | status | updated | links | |||
|---|---|---|---|---|---|---|---|---|
| 0.1 | runbook | private | active | 2026-08-05 |
|
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-wsi 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 dlavikunja.okit.pl. -
scripts/npm/.envwypełniony poświadczeniami (patrzkb/runbooks/npm-api.md). -
Strefa
okit.plw Cloudflare (patrzkb/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/.envz 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.py są dry-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.