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>
293 lines
12 KiB
Markdown
293 lines
12 KiB
Markdown
---
|
|
okf: "0.1"
|
|
type: runbook
|
|
visibility: private
|
|
status: active
|
|
updated: 2026-08-05
|
|
links:
|
|
- ../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
|
|
|
|
```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-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
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```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-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):
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
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):
|
|
|
|
```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.
|