docs(kb): kb-site — dokument serwisu (public) + runbook deployu (private)
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.
This commit is contained in:
parent
2c5894d70b
commit
41d3543082
248
kb/runbooks/kb-site-deploy.md
Normal file
248
kb/runbooks/kb-site-deploy.md
Normal file
|
|
@ -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: <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.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.
|
||||
74
kb/services/kb-site.md
Normal file
74
kb/services/kb-site.md
Normal file
|
|
@ -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
|
||||
<directory>/<name>.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.
|
||||
Loading…
Reference in a new issue