Compare commits
2 commits
04251b5bfb
...
bb3792d219
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb3792d219 | ||
|
|
f5c6f3b651 |
|
|
@ -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`.
|
przekierowany na PIHA — tak samo jak dla `vikunja.okit.pl`.
|
||||||
- `scripts/npm/.env` wypełniony poświadczeniami (patrz `kb/runbooks/npm-api.md`).
|
- `scripts/npm/.env` wypełniony poświadczeniami (patrz `kb/runbooks/npm-api.md`).
|
||||||
- Strefa `okit.pl` w Cloudflare (patrz `kb/phases/okit-cloudflare.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**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -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.
|
Generujemy w checkoucie repo, na węźle z którego pracujesz — nie na PIHA.
|
||||||
|
|
||||||
```bash
|
```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 --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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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,
|
`--check` skanuje **wygenerowany HTML**, nie źródła: adresy IP (RFC1918,
|
||||||
Tailscale 100.64/10, publiczne v4/v6), porty 1024-65535, ścieżki `/home/`
|
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.
|
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
|
## 6. Weryfikacja końcowa
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sI https://kb-e2a24af3.okit.pl/ | head -1 # 200
|
set -a; . /opt/homelab/config/kb-site/.env; set +a
|
||||||
curl -s https://kb-e2a24af3.okit.pl/ | grep -c 'class="cards"' # spis jest
|
BASE=https://kb-e2a24af3.okit.pl
|
||||||
curl -s https://kb-e2a24af3.okit.pl/subsystems/observer.html | tail -5 # stopka: data + commit
|
|
||||||
|
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
|
Ostatni test jest właściwą weryfikacją tej bramki: sprawdza, że link *wygenerowany
|
||||||
ostrzeżeń, wejście w dowolną kartę, powrót linkiem „All documents".
|
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
|
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
|
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`:
|
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-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 \
|
&& 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 . \
|
&& 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 \
|
||||||
&& ssh piha 'docker run --rm -v kb-site_kb-site_content:/content -v /tmp:/src:ro \
|
&& 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
|
&& 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.
|
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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| `--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 |
|
| cert nie schodzi | rekord A nie propagował, proxy Cloudflare włączone, albo port 80 niedostępny | krok 4, potem 5b ponownie |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`
|
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.
|
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,
|
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
|
browser history and outbound `Referer` headers, and — since the links carry it —
|
||||||
keeps it; nothing here resists a deliberate attacker. The leak gate above, not
|
into the body of every served page, so a single saved page or shared screenshot
|
||||||
this, is what keeps private material off the site.
|
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
|
## Operations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ Wyjście: build/kb-site/
|
||||||
|
|
||||||
Stopka każdej strony: data generacji + krótki hash commita (`git rev-parse --short HEAD`).
|
Stopka każdej strony: data generacji + krótki hash commita (`git rev-parse --short HEAD`).
|
||||||
|
|
||||||
|
Wystawka stoi za bramką NPM na `?key=<token>`, więc każdy link wewnętrzny musi
|
||||||
|
nieść ten token — inaczej kliknięcie ze spisu wpada w 403 i działa wyłącznie
|
||||||
|
ręcznie sklejony URL. Token podaje się przy generacji (`--access-token` albo
|
||||||
|
zmienna `ACCESS_TOKEN`) i NIGDY nie trafia do repo: to parametr runtime, nie
|
||||||
|
stała w kodzie. Bez tokenu strony generują się jak dotąd, z gołymi linkami —
|
||||||
|
to jest tryb lokalnego podglądu, nie błąd.
|
||||||
|
|
||||||
Tryb `--check` nie generuje niczego — skanuje JUŻ WYGENEROWANY katalog wyjściowy
|
Tryb `--check` nie generuje niczego — skanuje JUŻ WYGENEROWANY katalog wyjściowy
|
||||||
w poszukiwaniu wycieków (adresy IP, porty, ścieżki hosta, tokeny). Świadome
|
w poszukiwaniu wycieków (adresy IP, porty, ścieżki hosta, tokeny). Świadome
|
||||||
wyjątki trzymamy w scripts/kb/check_whitelist.txt. Trafienie = exit 1.
|
wyjątki trzymamy w scripts/kb/check_whitelist.txt. Trafienie = exit 1.
|
||||||
|
|
@ -35,7 +42,7 @@ frontmattera jest współdzielony z check_okf.py, żeby obie ścieżki widziały
|
||||||
frontmatter dokładnie tak samo.
|
frontmatter dokładnie tak samo.
|
||||||
|
|
||||||
Uruchomienie:
|
Uruchomienie:
|
||||||
python3 scripts/kb/gen_pages.py [--base-url URL] [--out KATALOG]
|
python3 scripts/kb/gen_pages.py [--base-url URL] [--out KATALOG] [--access-token TOKEN]
|
||||||
python3 scripts/kb/gen_pages.py --check [--out KATALOG] [--whitelist PLIK]
|
python3 scripts/kb/gen_pages.py --check [--out KATALOG] [--whitelist PLIK]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -43,11 +50,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import html
|
import html
|
||||||
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -87,6 +96,43 @@ TYPE_ORDER = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- token dostępu ----------------------------------------------------
|
||||||
|
|
||||||
|
ACCESS_TOKEN_ENV = "ACCESS_TOKEN"
|
||||||
|
ACCESS_TOKEN_PARAM = "key"
|
||||||
|
|
||||||
|
# Token bramki NPM. Ustawiany raz na starcie build(), czytany przez with_token().
|
||||||
|
# Modułowy, bo wewnętrzne href-y powstają w trzech miejscach na dnie rekurencji
|
||||||
|
# renderera (render_inline → render_blocks → render_blocks …); przewlekanie go
|
||||||
|
# parametrem przez cały renderer zaśmieciłoby każdą sygnaturę po drodze.
|
||||||
|
# Wartość NIGDY nie jest zapisywana w repo — pochodzi z --access-token/ACCESS_TOKEN.
|
||||||
|
_ACCESS_TOKEN = ""
|
||||||
|
|
||||||
|
|
||||||
|
def set_access_token(token: str) -> None:
|
||||||
|
global _ACCESS_TOKEN
|
||||||
|
_ACCESS_TOKEN = token or ""
|
||||||
|
|
||||||
|
|
||||||
|
def with_token(href: str) -> str:
|
||||||
|
"""Dopisuje `?key=<token>` do wewnętrznego odnośnika.
|
||||||
|
|
||||||
|
Bez ustawionego tokenu zwraca href bez zmian — generacja lokalna do podglądu
|
||||||
|
ma dawać dokładnie to co dotąd. Fragment (`#sekcja`) zostaje na końcu, bo
|
||||||
|
query string idzie PRZED kotwicą. Gdyby URL kiedyś niósł własne parametry,
|
||||||
|
doklejamy `&` zamiast `?` — dziś nie niesie, ale to jeden warunek.
|
||||||
|
"""
|
||||||
|
if not _ACCESS_TOKEN:
|
||||||
|
return href
|
||||||
|
base, hash_sep, fragment = href.partition("#")
|
||||||
|
if not base:
|
||||||
|
# Czysta kotwica w obrębie strony — nie ma czego bramkować.
|
||||||
|
return href
|
||||||
|
sep = "&" if "?" in base else "?"
|
||||||
|
token = urllib.parse.quote(_ACCESS_TOKEN, safe="")
|
||||||
|
return f"{base}{sep}{ACCESS_TOKEN_PARAM}={token}{hash_sep}{fragment}"
|
||||||
|
|
||||||
|
|
||||||
# --- model ------------------------------------------------------------
|
# --- model ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -242,6 +288,9 @@ def render_inline(text: str) -> str:
|
||||||
attrs = ""
|
attrs = ""
|
||||||
if href.startswith(("http://", "https://", "mailto:")):
|
if href.startswith(("http://", "https://", "mailto:")):
|
||||||
attrs = ' target="_blank" rel="noopener" class="ext"'
|
attrs = ' target="_blank" rel="noopener" class="ext"'
|
||||||
|
else:
|
||||||
|
# Wewnętrzny (strona .html albo kotwica) — musi nieść token bramki.
|
||||||
|
href = with_token(href)
|
||||||
return f'<a href="{html.escape(href, quote=True)}"{attrs}>{label}</a>'
|
return f'<a href="{html.escape(href, quote=True)}"{attrs}>{label}</a>'
|
||||||
|
|
||||||
text = _LINK_RE.sub(link, text)
|
text = _LINK_RE.sub(link, text)
|
||||||
|
|
@ -457,7 +506,8 @@ def page_shell(
|
||||||
) -> str:
|
) -> str:
|
||||||
up = "../" * depth
|
up = "../" * depth
|
||||||
if nav is None:
|
if nav is None:
|
||||||
nav = f'<a href="{up}index.html">← All documents</a>'
|
back = html.escape(with_token(f"{up}index.html"), quote=True)
|
||||||
|
nav = f'<a href="{back}">← All documents</a>'
|
||||||
nav_html = f"<nav>{nav}</nav>\n" if nav else ""
|
nav_html = f"<nav>{nav}</nav>\n" if nav else ""
|
||||||
return f"""<!doctype html>
|
return f"""<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
@ -546,7 +596,7 @@ def render_index(docs: list[Doc], base_url: str, stamp: str) -> str:
|
||||||
for doc in items:
|
for doc in items:
|
||||||
lead = summary(doc)
|
lead = summary(doc)
|
||||||
cards.append(
|
cards.append(
|
||||||
f'<li><a href="{html.escape(doc.page, quote=True)}">'
|
f'<li><a href="{html.escape(with_token(doc.page), quote=True)}">'
|
||||||
f"{html.escape(doc.title)}</a>"
|
f"{html.escape(doc.title)}</a>"
|
||||||
f'<span class="chip">{html.escape(doc.doc_type)}</span>'
|
f'<span class="chip">{html.escape(doc.doc_type)}</span>'
|
||||||
+ (f"<p>{html.escape(lead)}</p>" if lead else "")
|
+ (f"<p>{html.escape(lead)}</p>" if lead else "")
|
||||||
|
|
@ -589,7 +639,8 @@ def git_commit() -> str:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
def build(out_dir: Path, base_url: str) -> list[Doc]:
|
def build(out_dir: Path, base_url: str, access_token: str = "") -> list[Doc]:
|
||||||
|
set_access_token(access_token)
|
||||||
docs = load_docs()
|
docs = load_docs()
|
||||||
public = [d for d in docs if d.public]
|
public = [d for d in docs if d.public]
|
||||||
published = {d.id: d.page for d in public}
|
published = {d.id: d.page for d in public}
|
||||||
|
|
@ -618,6 +669,11 @@ def build(out_dir: Path, base_url: str) -> list[Doc]:
|
||||||
print(f"Źródło: {KB_DIR.relative_to(REPO_ROOT)}/**/*.md")
|
print(f"Źródło: {KB_DIR.relative_to(REPO_ROOT)}/**/*.md")
|
||||||
print(f"Wyjście: {out_dir}")
|
print(f"Wyjście: {out_dir}")
|
||||||
print(f"BASE_URL: {base_url}")
|
print(f"BASE_URL: {base_url}")
|
||||||
|
# Sam token nigdy nie leci na stdout — logi z generacji bywają wklejane.
|
||||||
|
print(
|
||||||
|
f"Token: {'TAK' if _ACCESS_TOKEN else 'NIE'} "
|
||||||
|
f"(linki wewnętrzne {'z' if _ACCESS_TOKEN else 'bez'} ?{ACCESS_TOKEN_PARAM}=…)"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f"Dokumenty: {len(docs)} razem, {len(public)} public, "
|
f"Dokumenty: {len(docs)} razem, {len(public)} public, "
|
||||||
f"{len(docs) - len(public)} pominiętych (private / brak frontmattera)"
|
f"{len(docs) - len(public)} pominiętych (private / brak frontmattera)"
|
||||||
|
|
@ -671,6 +727,9 @@ _NEUTRAL_IPV6 = {"::", "::1"}
|
||||||
# tylko `::` z adresów IPv6; te i tak raportuje wzorzec `ip-v6`.
|
# tylko `::` z adresów IPv6; te i tak raportuje wzorzec `ip-v6`.
|
||||||
_PORT_RE = re.compile(r"(?<!:):(\d{4,5})(?!\d)")
|
_PORT_RE = re.compile(r"(?<!:):(\d{4,5})(?!\d)")
|
||||||
_PATH_RE = re.compile(r"/(?:home|opt)/[\w.\-/]*")
|
_PATH_RE = re.compile(r"/(?:home|opt)/[\w.\-/]*")
|
||||||
|
_HREF_TOKEN_RE = re.compile(
|
||||||
|
rf'(href="[^"]*[?&]{re.escape(ACCESS_TOKEN_PARAM)}=)[^"&#]*'
|
||||||
|
)
|
||||||
_HEX_TOKEN_RE = re.compile(r"(?<![\w])[0-9a-fA-F]{32,}(?![\w])")
|
_HEX_TOKEN_RE = re.compile(r"(?<![\w])[0-9a-fA-F]{32,}(?![\w])")
|
||||||
_B64_TOKEN_RE = re.compile(r"(?<![\w+/=-])[A-Za-z0-9+/_-]{40,}={0,2}(?![\w+/=-])")
|
_B64_TOKEN_RE = re.compile(r"(?<![\w+/=-])[A-Za-z0-9+/_-]{40,}={0,2}(?![\w+/=-])")
|
||||||
|
|
||||||
|
|
@ -728,6 +787,13 @@ def _token_hits(text: str) -> list[tuple[str, str]]:
|
||||||
|
|
||||||
|
|
||||||
def scan_line(text: str) -> list[tuple[str, str]]:
|
def scan_line(text: str) -> list[tuple[str, str]]:
|
||||||
|
# Token bramki w linkach wewnętrznych to nie wyciek, tylko cały sens tych
|
||||||
|
# linków — a wygląda dokładnie jak sekret, na który poluje `token-hex`/
|
||||||
|
# `token-b64`. Wycinamy WYŁĄCZNIE wartość `key=` wewnątrz atrybutu href;
|
||||||
|
# reszta linii (i każde inne `key=` w treści dokumentu) leci do skanera
|
||||||
|
# normalnie, żeby ta furtka nie zaczęła wyciszać prawdziwych sekretów.
|
||||||
|
text = _HREF_TOKEN_RE.sub(r"\1", text)
|
||||||
|
|
||||||
hits = _ipv4_hits(text) + _ipv6_hits(text) + _port_hits(text)
|
hits = _ipv4_hits(text) + _ipv6_hits(text) + _port_hits(text)
|
||||||
hits += [("path-host", m.group(0)) for m in _PATH_RE.finditer(text)]
|
hits += [("path-host", m.group(0)) for m in _PATH_RE.finditer(text)]
|
||||||
hits += _token_hits(text)
|
hits += _token_hits(text)
|
||||||
|
|
@ -830,6 +896,15 @@ def main() -> int:
|
||||||
default=DEFAULT_OUT,
|
default=DEFAULT_OUT,
|
||||||
help="katalog wyjściowy (domyślnie build/kb-site)",
|
help="katalog wyjściowy (domyślnie build/kb-site)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--access-token",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
f"token bramki NPM dopisywany jako ?{ACCESS_TOKEN_PARAM}=… do każdego "
|
||||||
|
f"linku wewnętrznego; domyślnie ze zmiennej {ACCESS_TOKEN_ENV}. "
|
||||||
|
"Bez tokenu linki zostają gołe (podgląd lokalny)."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--check",
|
"--check",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -846,7 +921,10 @@ def main() -> int:
|
||||||
if args.check:
|
if args.check:
|
||||||
return check(args.out.resolve(), args.whitelist)
|
return check(args.out.resolve(), args.whitelist)
|
||||||
|
|
||||||
build(args.out.resolve(), args.base_url)
|
token = args.access_token
|
||||||
|
if token is None:
|
||||||
|
token = os.environ.get(ACCESS_TOKEN_ENV, "")
|
||||||
|
build(args.out.resolve(), args.base_url, token.strip())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
# The port bind (8250:80) is static and the content lives in the
|
||||||
# kb-site_kb-site_content Docker volume, generated from kb/ by
|
# kb-site_kb-site_content Docker volume, generated from kb/ by
|
||||||
# scripts/kb/gen_pages.py. This file exists only to keep the
|
# scripts/kb/gen_pages.py. Nothing here is copied to a .env on PIHA.
|
||||||
# services/<service>/ layout from CLAUDE.md complete — there is nothing to
|
|
||||||
# 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-e2a24af3.okit.pl
|
# 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=<token bramki NPM>
|
||||||
|
#
|
||||||
|
# The site sits behind a query-parameter gate in NPM. gen_pages.py reads
|
||||||
|
# ACCESS_TOKEN from the environment and appends ?key=<token> 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).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ service:
|
||||||
paths:
|
paths:
|
||||||
- kb-site_kb-site_content
|
- kb-site_kb-site_content
|
||||||
runtime:
|
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: []
|
env_vars: []
|
||||||
content:
|
content:
|
||||||
# Only kb/ documents with `visibility: public` are published; the generator
|
# Only kb/ documents with `visibility: public` are published; the generator
|
||||||
|
|
@ -31,3 +34,11 @@ service:
|
||||||
generator: scripts/kb/gen_pages.py
|
generator: scripts/kb/gen_pages.py
|
||||||
leak_check: scripts/kb/gen_pages.py --check # must pass before publishing
|
leak_check: scripts/kb/gen_pages.py --check # must pass before publishing
|
||||||
source: kb/**/*.md
|
source: kb/**/*.md
|
||||||
|
# NPM enforces a ?key=<token> 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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue