Compare commits

...

2 commits

Author SHA1 Message Date
oskar 67e49a0952 docs(kb-site): przepisz nieaktualne kb.okit.pl na kb-e2a24af3.okit.pl
Dociagniecie do zmiany DEFAULT_BASE_URL z 24afb49 — po niej repo w szesciu
miejscach dalej podawalo stary adres.

- kb/runbooks/kb-site-deploy.md: wszystkie wystapienia + rekord Cloudflare
  (Name: kb -> kb-e2a24af3). Runbook jest visibility: private, wiec slug
  moze stac wprost.
- services/kb-site/{README.md,service.yaml,env.example,docker-compose.yml}
- hosts/piha/services.yaml: komentarz przy exposure

kb/services/kb-site.md swiadomie nietkniety — dokument publiczny, slug tam
nie wchodzi (opisuje adres jako "non-obvious subdomain").

check_okf.py exit 0, gen_pages.py --check exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:08:13 +02:00
oskar db81cb15d3 feat(kb-site): noindex + robots.txt + obscure subdomain jako domyslny base-url
Warstwa "nie daj sie przypadkiem znalezc" dla publicznej wystawki KB:

- gen_pages.py: <meta name="robots" content="noindex, nofollow"> w <head>
  kazdej generowanej strony (page_shell, wiec takze index).
- gen_pages.py: DEFAULT_BASE_URL -> https://kb-e2a24af3.okit.pl. Slug musi
  zgadzac sie z rekordem DNS i vhostem w npm@PIHA (runbook kb-site-deploy).
- services/kb-site: static/robots.txt (Disallow: /) montowany ro na
  /usr/share/nginx/html/robots.txt. Plik nie jest dokumentem KB, wiec jedzie
  z repo, a nie z wolumenu podmienianego przy kazdej publikacji.
- kb/services/kb-site.md: sekcja "Access" — token w query paramie na warstwie
  nginx/NPM (sekret zyje tylko w NPM, nie w repo) + obscure subdomain +
  noindex. Explicit: to obscurity, nie kontrola dostepu — token w URL laduje
  w access logach, historii przegladarki i naglowku Referer.

Bramka publikacji bez zmian: gen_pages.py --check exit 0 (22 wyciszone
whitelista, jak dotad).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:08:13 +02:00
9 changed files with 47 additions and 26 deletions

View file

@ -168,7 +168,7 @@ services:
kb-site: kb-site:
role: static-html-host # public KB slice generated by scripts/kb/gen_pages.py role: static-html-host # public KB slice generated by scripts/kb/gen_pages.py
deployment_model: docker-compose deployment_model: docker-compose
exposure: public # via npm@PIHA vhost kb.okit.pl; container binds LAN :8250 exposure: public # via npm@PIHA vhost kb-e2a24af3.okit.pl; container binds LAN :8250
offline_required: false offline_required: false
depends_on: depends_on:
local: [] local: []

View file

@ -3,17 +3,17 @@ okf: "0.1"
type: runbook type: runbook
visibility: private visibility: private
status: active status: active
updated: 2026-08-04 updated: 2026-08-05
links: links:
- ../services/kb-site.md - ../services/kb-site.md
- ./npm-api.md - ./npm-api.md
- ../phases/okit-cloudflare.md - ../phases/okit-cloudflare.md
--- ---
# kb-site — deploy, publikacja treści, ingress (kb.okit.pl) # kb-site — deploy, publikacja treści, ingress (kb-e2a24af3.okit.pl)
Serwis: `services/kb-site/` (nginx:alpine na PIHA, host port **8250**). Serwis: `services/kb-site/` (nginx:alpine na PIHA, host port **8250**).
Generator: `scripts/kb/gen_pages.py`. Publiczny adres: `https://kb.okit.pl`. Generator: `scripts/kb/gen_pages.py`. Publiczny adres: `https://kb-e2a24af3.okit.pl`.
Kolejność ma znaczenie: **DNS → NPM host → cert**. Certyfikat Let's Encrypt 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, leci challenge'em HTTP-01, więc rekord A musi już wskazywać na łącze domowe,
@ -57,7 +57,7 @@ docker volume ls | grep kb-site # kb-site_kb-site_content
Generujemy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA. Generujemy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA.
```bash ```bash
python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl
python3 scripts/kb/gen_pages.py --check python3 scripts/kb/gen_pages.py --check
echo $? # 0 = czysto, 1 = trafienia echo $? # 0 = czysto, 1 = trafienia
``` ```
@ -130,7 +130,7 @@ curl -sI http://192.168.31.5:8250/index.html | head -1
``` ```
Type: A Type: A
Name: kb Name: kb-e2a24af3
Content: <publiczny IP łącza domowego> # ten sam, co ma vikunja.okit.pl Content: <publiczny IP łącza domowego> # ten sam, co ma vikunja.okit.pl
Proxy: DNS only (szara chmurka) Proxy: DNS only (szara chmurka)
TTL: Auto TTL: Auto
@ -144,13 +144,13 @@ a nie na krawędź Cloudflare. Włączenie proxy to osobna decyzja, po tym jak c
już działa. już działa.
Split-horizon (patrz `kb/phases/okit-cloudflare.md`): w Pi-hole na PIHA dodaj 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 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. npm, a nie przez hairpin NAT na routerze.
Sprawdzenie propagacji (z hosta poza LAN albo przez publiczny resolver): Sprawdzenie propagacji (z hosta poza LAN albo przez publiczny resolver):
```bash ```bash
dig +short kb.okit.pl @1.1.1.1 dig +short kb-e2a24af3.okit.pl @1.1.1.1
``` ```
--- ---
@ -161,15 +161,15 @@ Wszystkie komendy `npm_api.py` są **dry-run domyślnie**; realna zmiana dopiero
z `--apply`. Puść najpierw bez `--apply` i przeczytaj payload. z `--apply`. Puść najpierw bez `--apply` i przeczytaj payload.
```bash ```bash
# 5a. host: kb.okit.pl -> PIHA:8250 # 5a. host: kb-e2a24af3.okit.pl -> PIHA:8250
python3 scripts/npm/npm_api.py --npm piha create-host \ python3 scripts/npm/npm_api.py --npm piha create-host \
--domain kb.okit.pl \ --domain kb-e2a24af3.okit.pl \
--forward-host 192.168.31.5 --forward-port 8250 \ --forward-host 192.168.31.5 --forward-port 8250 \
--block-exploits --http2-support --block-exploits --http2-support
# ...i to samo z --apply # ...i to samo z --apply
# 5b. cert Let's Encrypt (HTTP-01 — wymaga działającego kroku 4 i 5a) # 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 python3 scripts/npm/npm_api.py --npm piha create-cert --domain kb-e2a24af3.okit.pl --apply
# 5c. podepnij cert pod host i wymuś HTTPS # 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-hosts # weź HOST_ID
@ -190,12 +190,12 @@ wiersz certyfikatu (szczegóły w `kb/runbooks/npm-api.md`).
## 6. Weryfikacja końcowa ## 6. Weryfikacja końcowa
```bash ```bash
curl -sI https://kb.okit.pl/ | head -1 # 200 curl -sI https://kb-e2a24af3.okit.pl/ | head -1 # 200
curl -s https://kb.okit.pl/ | grep -c 'class="cards"' # spis jest curl -s https://kb-e2a24af3.okit.pl/ | grep -c 'class="cards"' # spis jest
curl -s https://kb.okit.pl/subsystems/observer.html | tail -5 # stopka: data + commit curl -s https://kb-e2a24af3.okit.pl/subsystems/observer.html | tail -5 # stopka: data + commit
``` ```
W przeglądarce: `https://kb.okit.pl` — spis pogrupowany per type, kłódka bez W przeglądarce: `https://kb-e2a24af3.okit.pl` — spis pogrupowany per type, kłódka bez
ostrzeżeń, wejście w dowolną kartę, powrót linkiem „All documents". ostrzeżeń, wejście w dowolną kartę, powrót linkiem „All documents".
Kontrola treści (ręczna, jednorazowa po pierwszej publikacji): przejrzyj spis Kontrola treści (ręczna, jednorazowa po pierwszej publikacji): przejrzyj spis
@ -209,7 +209,7 @@ pilnuje pola `visibility`, ale to człowiek decyduje, co dostaje `public`.
Po każdej zmianie w `kb/**/*.md`, która dotyczy dokumentów `public`: Po każdej zmianie w `kb/**/*.md`, która dotyczy dokumentów `public`:
```bash ```bash
python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl \ python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl \
&& python3 scripts/kb/gen_pages.py --check \ && python3 scripts/kb/gen_pages.py --check \
&& tar -czf /tmp/kb-site.tgz -C build/kb-site . \ && tar -czf /tmp/kb-site.tgz -C build/kb-site . \
&& scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz \ && scp /tmp/kb-site.tgz piha:/tmp/kb-site.tgz \

View file

@ -3,14 +3,15 @@ okf: "0.1"
type: service type: service
visibility: public visibility: public
status: active status: active
updated: 2026-08-04 updated: 2026-08-05
links: links:
- ../runbooks/kb-site-deploy.md - ../runbooks/kb-site-deploy.md
--- ---
# kb-site # kb-site
Public slice of this knowledge base, served as static HTML at `kb.okit.pl`. Public slice of this knowledge base, served as static HTML from a deliberately
non-obvious subdomain of `okit.pl`.
Plain `nginx:alpine` on the PIHA node reading one Docker named volume — no Plain `nginx:alpine` on the PIHA node reading one Docker named volume — no
build step at runtime, no database, no dependencies. build step at runtime, no database, no dependencies.
@ -58,6 +59,18 @@ out empty, so every exception is a recorded decision.
The check is a release gate: content is copied to the host only after it The check is a release gate: content is copied to the host only after it
passes. passes.
## Access
Three layers keep the site out of casual sight: a query-parameter token enforced
in the nginx/NPM layer (advanced config held in NPM only — the secret is not in
this repository), an unguessable subdomain, and `noindex, nofollow` on every page
alongside a blanket `robots.txt`.
This is obscurity, not access control. A URL token is written to access logs,
browser history and outbound `Referer` headers, so anyone who obtains a link
keeps it; nothing here resists a deliberate attacker. The leak gate above, not
this, is what keeps private material off the site.
## Operations ## Operations
Deployment, content refresh, reverse-proxy and DNS setup are described in the Deployment, content refresh, reverse-proxy and DNS setup are described in the

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generator publicznej warstwy bazy wiedzy (kb.okit.pl) z dokumentów OKF v0.1. """Generator publicznej warstwy bazy wiedzy z dokumentów OKF v0.1.
Wzorzec: ~/narty-2027/saalbach-kb/gen_pages.py renderer markdown na samej Wzorzec: ~/narty-2027/saalbach-kb/gen_pages.py renderer markdown na samej
bibliotece standardowej plus prosty, czytelny szablon HTML. Tutaj dochodzi bibliotece standardowej plus prosty, czytelny szablon HTML. Tutaj dochodzi
@ -60,7 +60,7 @@ from check_okf import parse_yaml, split_frontmatter # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
KB_DIR = REPO_ROOT / "kb" KB_DIR = REPO_ROOT / "kb"
DEFAULT_OUT = REPO_ROOT / "build" / "kb-site" DEFAULT_OUT = REPO_ROOT / "build" / "kb-site"
DEFAULT_BASE_URL = "https://kb.okit.pl" DEFAULT_BASE_URL = "https://kb-e2a24af3.okit.pl"
DEFAULT_WHITELIST = Path(__file__).resolve().parent / "check_whitelist.txt" DEFAULT_WHITELIST = Path(__file__).resolve().parent / "check_whitelist.txt"
SITE_NAME = "homelab-codex — knowledge base" SITE_NAME = "homelab-codex — knowledge base"
@ -464,6 +464,7 @@ def page_shell(
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>{html.escape(title)} {html.escape(SITE_NAME)}</title> <title>{html.escape(title)} {html.escape(SITE_NAME)}</title>
<link rel="canonical" href="{html.escape(canonical, quote=True)}"> <link rel="canonical" href="{html.escape(canonical, quote=True)}">
<style>{CSS}</style> <style>{CSS}</style>
@ -816,7 +817,7 @@ def check(out_dir: Path, whitelist_path: Path) -> int:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Generator publicznej warstwy KB (kb.okit.pl) z dokumentów OKF." description="Generator publicznej warstwy KB z dokumentów OKF."
) )
parser.add_argument( parser.add_argument(
"--base-url", "--base-url",

View file

@ -1,6 +1,6 @@
# kb-site # kb-site
Public slice of the knowledge base (`kb.okit.pl`) — static HTML generated from `kb/**/*.md` by `scripts/kb/gen_pages.py`, served by nginx on PIHA. Public slice of the knowledge base (`kb-e2a24af3.okit.pl`) — static HTML generated from `kb/**/*.md` by `scripts/kb/gen_pages.py`, served by nginx on PIHA.
## Sieć ## Sieć
@ -15,7 +15,7 @@ Warunek wstępny deployu — sieć musi już istnieć na hoście:
docker network ls | grep -w proxy # brak wyniku => docker network create proxy docker network ls | grep -w proxy # brak wyniku => docker network create proxy
``` ```
Ruch publiczny i tak nie idzie przez tę sieć: npm@PIHA (vhost `kb.okit.pl`) trafia Ruch publiczny i tak nie idzie przez tę sieć: npm@PIHA (vhost `kb-e2a24af3.okit.pl`) trafia
do kontenera po opublikowanym porcie hosta `8250`. do kontenera po opublikowanym porcie hosta `8250`.
Dokumentacja: [kb/services/kb-site.md](../../kb/services/kb-site.md) Dokumentacja: [kb/services/kb-site.md](../../kb/services/kb-site.md)

View file

@ -7,7 +7,7 @@ services:
# PIHA 82x0 static-HTTP block: 8210 paperless, 8220 nextcloud, # PIHA 82x0 static-HTTP block: 8210 paperless, 8220 nextcloud,
# 8230 kb-query, 8240 narty27 -> 8250 is the next free slot. # 8230 kb-query, 8240 narty27 -> 8250 is the next free slot.
# Publicly the site is reached only through the npm@PIHA vhost # Publicly the site is reached only through the npm@PIHA vhost
# kb.okit.pl; this bind is the proxy's upstream. # kb-e2a24af3.okit.pl; this bind is the proxy's upstream.
- "8250:80" - "8250:80"
volumes: volumes:
# Generated output of scripts/kb/gen_pages.py — never committed, never # Generated output of scripts/kb/gen_pages.py — never committed, never
@ -15,6 +15,11 @@ services:
# through the helper-container procedure in kb/runbooks/kb-site-deploy.md # through the helper-container procedure in kb/runbooks/kb-site-deploy.md
# (docker cp cannot write into a :ro mount). # (docker cp cannot write into a :ro mount).
- kb-site_content:/usr/share/nginx/html:ro - kb-site_content:/usr/share/nginx/html:ro
# robots.txt nie pochodzi z generatora — nie jest dokumentem KB, więc
# jedzie z repo i nie ginie przy podmianie zawartości wolumenu. Mount
# pliku jest głębszy niż mount katalogu, więc Docker montuje go później
# i plik przykrywa (nieistniejące) robots.txt z wolumenu.
- ./static/robots.txt:/usr/share/nginx/html/robots.txt:ro
# Docker on PIHA has exhausted its default address pools (~30 live stacks: # Docker on PIHA has exhausted its default address pools (~30 live stacks:
# "all predefined address pools have been fully subnetted"), so this stack # "all predefined address pools have been fully subnetted"), so this stack
# must not ask for a subnet of its own. Declaring a network here suppresses # must not ask for a subnet of its own. Declaring a network here suppresses

View file

@ -7,4 +7,4 @@
# copy to .env. # copy to .env.
# #
# The public address is a generator argument, not an env var: # The public address is a generator argument, not an env var:
# python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl # python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl

View file

@ -2,7 +2,7 @@ service:
name: kb-site name: kb-site
owner_node: piha owner_node: piha
role: static-html-host # public slice of the KB, rendered by scripts/kb/gen_pages.py role: static-html-host # public slice of the KB, rendered by scripts/kb/gen_pages.py
exposure: public # public via npm@PIHA (kb.okit.pl). The container itself binds exposure: public # public via npm@PIHA (kb-e2a24af3.okit.pl). The container itself binds
# 8250 on the LAN; npm is the sole public entry point. # 8250 on the LAN; npm is the sole public entry point.
dependencies: [] # nginx serving a local volume — nothing else required at runtime dependencies: [] # nginx serving a local volume — nothing else required at runtime
ports: ports:

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /