diff --git a/kb/runbooks/kb-site-deploy.md b/kb/runbooks/kb-site-deploy.md index 3befba6..8bed1d1 100644 --- a/kb/runbooks/kb-site-deploy.md +++ b/kb/runbooks/kb-site-deploy.md @@ -28,6 +28,15 @@ a vhost musi już odpowiadać na porcie 80, zanim zamówisz cert. 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=`, `chmod 600`. To wyjątek od konwencji z CLAUDE.md: + `config//` 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**. --- @@ -57,11 +66,44 @@ 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. ```bash +set -a; . /opt/homelab/config/kb-site/.env; set +a # ACCESS_TOKEN +test -n "$ACCESS_TOKEN" || echo "BRAK TOKENU — nie publikuj tego builda" + python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl python3 scripts/kb/gen_pages.py --check echo $? # 0 = czysto, 1 = trafienia ``` +### Token w linkach + +Generator czyta `ACCESS_TOKEN` ze środowiska i dopisuje `?key=` 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. @@ -190,13 +232,29 @@ wiersz certyfikatu (szczegóły w `kb/runbooks/npm-api.md`). ## 6. Weryfikacja końcowa ```bash -curl -sI https://kb-e2a24af3.okit.pl/ | head -1 # 200 -curl -s https://kb-e2a24af3.okit.pl/ | grep -c 'class="cards"' # spis jest -curl -s https://kb-e2a24af3.okit.pl/subsystems/observer.html | tail -5 # stopka: data + commit +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 ``` -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". +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=` — 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 @@ -209,8 +267,12 @@ pilnuje pola `visibility`, ale to człowiek decyduje, co dostaje `public`. Po każdej zmianie w `kb/**/*.md`, która dotyczy dokumentów `public`: ```bash -python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl \ +set -a; . /opt/homelab/config/kb-site/.env; set +a # ACCESS_TOKEN + +test -n "$ACCESS_TOKEN" \ + && python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl \ && python3 scripts/kb/gen_pages.py --check \ + && grep -q 'key=' build/kb-site/index.html \ && 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 \ @@ -219,7 +281,9 @@ python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl \ && rm /tmp/kb-site.tgz ``` -Łańcuch na `&&` jest celowy: `--check` z exit 1 zatrzymuje publikację. +Łańcuch na `&&` jest celowy — każde ogniwo zatrzymuje publikację: pusty +`ACCESS_TOKEN` (build byłby nieklikalny), `--check` z exit 1 (wyciek), brak +`key=` w spisie (token był pusty albo nie dojechał do generatora). Kontener nie wymaga restartu — nginx czyta wolumen na bieżąco. @@ -233,6 +297,8 @@ Kontener nie wymaga restartu — nginx czyta wolumen na bieżąco. | 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 | diff --git a/kb/services/kb-site.md b/kb/services/kb-site.md index 2dfe0f5..1ca1035 100644 --- a/kb/services/kb-site.md +++ b/kb/services/kb-site.md @@ -72,10 +72,18 @@ out precisely the readers the token exists to let in. It also worked against its own goal: a crawler blocked before it can read the page never sees the `noindex` tag, and a search engine may still list a bare URL it was forbidden to fetch. +Because the gate is enforced on the URL, every internal link the generator emits +carries the token too — otherwise the entry page would open and every click from +it would return 403. The token is therefore a generation-time input, supplied +from outside the repository; a build made without it is a local preview, not a +publishable site. + 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. +browser history and outbound `Referer` headers, and — since the links carry it — +into the body of every served page, so a single saved page or shared screenshot +of the address bar hands it over in full. 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 diff --git a/services/kb-site/env.example b/services/kb-site/env.example index 5a7759b..dadb74c 100644 --- a/services/kb-site/env.example +++ b/services/kb-site/env.example @@ -1,10 +1,27 @@ -# kb-site has NO configuration and NO secrets. +# The kb-site CONTAINER has no configuration and no secrets. # # The port bind (8250:80) is static and the content lives in the # kb-site_kb-site_content Docker volume, generated from kb/ by -# scripts/kb/gen_pages.py. This file exists only to keep the -# services// layout from CLAUDE.md complete — there is nothing to -# copy to .env. +# scripts/kb/gen_pages.py. Nothing here is copied to a .env on PIHA. # # The public address is a generator argument, not an env var: # python3 scripts/kb/gen_pages.py --base-url https://kb-e2a24af3.okit.pl +# +# --------------------------------------------------------------------------- +# GENERATION-TIME secret — belongs on the node you generate from +# (SATURN/SOLARIA), NOT on PIHA: +# +# /opt/homelab/config/kb-site/.env chmod 600 +# ACCESS_TOKEN= +# +# The site sits behind a query-parameter gate in NPM. gen_pages.py reads +# ACCESS_TOKEN from the environment and appends ?key= to every internal +# link; without it every click on the published site returns 403 and only a +# hand-assembled URL works. +# +# Same value as the advanced config of the proxy host in NPM. It lives in the +# NPM database and in that file only — never in this repository. Prefer the +# environment variable over the --access-token flag: a command-line argument +# lands in shell history and is visible in `ps`. +# +# Full procedure: kb/runbooks/kb-site-deploy.md (steps 0, 2 and 7). diff --git a/services/kb-site/service.yaml b/services/kb-site/service.yaml index 00fbacc..368008f 100644 --- a/services/kb-site/service.yaml +++ b/services/kb-site/service.yaml @@ -23,7 +23,10 @@ service: paths: - kb-site_kb-site_content runtime: - config_files: [] # no .env — the port bind is static, no secrets + # The CONTAINER has no .env and no secrets — the port bind is static. The + # access token below is not a container env var: it is consumed by the + # generator on the node that builds the site, never by nginx on PIHA. + config_files: [] env_vars: [] content: # Only kb/ documents with `visibility: public` are published; the generator @@ -31,3 +34,11 @@ service: generator: scripts/kb/gen_pages.py leak_check: scripts/kb/gen_pages.py --check # must pass before publishing source: kb/**/*.md + # NPM enforces a ?key= gate in front of this site, so every internal + # href must carry that token or a click lands on 403. The generator reads it + # from ACCESS_TOKEN; the value lives in the NPM database and in + # /opt/homelab/config/kb-site/.env ON THE GENERATING NODE (saturn/solaria) — + # never in this repository. Omitting it yields a valid preview build that + # must not be published. See kb/runbooks/kb-site-deploy.md. + access_token_env: ACCESS_TOKEN + access_token_file: /opt/homelab/config/kb-site/.env # generating node, not piha