Compare commits

...

2 commits

Author SHA1 Message Date
oskar bb3792d219 docs(kb-site): przekaz ACCESS_TOKEN generatorowi w procedurze publikacji
Poprzedni commit nauczyl gen_pages.py dopisywac token do linkow, ale nic
go nie podawalo — publikacja poszlaby stara sciezka i dalaby build
z golymi linkami, czyli stan sprzed fiksa.

kb-site nie ma skryptu deployu: generator wolany jest wylacznie recznie
z runbooka (kroki 2 i 7), wiec to tam token musi wejsc.

Zrodlo tokenu: /opt/homelab/config/kb-site/.env na wezle GENERUJACYM
(SATURN/SOLARIA), nie na PIHA. To swiadome odstepstwo od konwencji
config/<serwis>/ z CLAUDE.md — plik trzyma zwykle sekrety wezla, ktory
serwis uruchamia, a ten token jest potrzebny tam, gdzie serwis sie
generuje. Kontener nginx dalej nie ma zadnej konfiguracji ani sekretow;
odnotowane w service.yaml i env.example, zeby nikt nie szukal .env na PIHA.

Token idzie zmienna srodowiskowa (set -a; . plik; set +a), nie flaga
--access-token: argument z linii polecen laduje w historii shella i jest
widoczny w ps dla kazdego uzytkownika wezla.

Lancuch publikacji z kroku 7 dostal dwa nowe ogniwa przed scp: test -n
"$ACCESS_TOKEN" (pusty token = build nieklikalny) oraz grep -q 'key='
w index.html (token byl, ale nie dojechal do generatora). Oba zatrzymuja
publikacje tak samo jak --check.

Krok 6 weryfikuje teraz wlasciwa rzecz: wyciaga href ze spisu i pobiera
GO, zamiast recznie sklejac URL — czyli testuje to, co faktycznie bylo
zepsute. Doszedl tez negatywny test bramki (bez tokenu ma NIE byc 200).

Tabela problemow: "index sie otwiera, ale klikniecie daje 403" (build bez
tokenu) i "403 takze z tokenem" (rotacja tokenu w NPM rozjechana z plikiem
— stary build zostaje z martwym tokenem w kazdym linku).

kb/services/kb-site.md (public) — sekcja Access: token siedzi teraz
w tresci kazdej serwowanej strony, wiec jedna zapisana strona wydaje go
w calosci. Model zagrozen bez zmian (URL wejsciowy zawsze go niosl), ale
warto, zeby dokument mowil to wprost obok zdania "to obscurity, not
access control".

Test: sekwencje z krokow 2 i 7 przepuszczone na symulowanym pliku tokenu
(prod /opt/homelab nietkniety) — token obecny: Token: TAK, 4x key=
w index.html, lancuch dochodzi do tar; token pusty: staje na pierwszym
ogniwie, brak tgz; build bez tokenu przy ustawionej zmiennej: staje na
grep, brak tgz. check_okf.py exit 0, gen_pages --check exit 0,
service.yaml parsuje sie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:26:48 +02:00
oskar f5c6f3b651 fix(kb-site): token bramki w kazdym linku wewnetrznym generatora
Wystawka KB stoi za bramka NPM na ?key=<token>. Linki generowane przez
gen_pages.py (index -> dokument, dokument <-> dokument, powrot do indexu)
tokenu nie nosily, wiec kazde klikniecie ze strony wpadalo w 403 —
dzialal wylacznie recznie sklejony URL do indexu.

Token podaje sie przy generacji: --access-token TOKEN albo zmienna
ACCESS_TOKEN. Nie ma go w repo w zadnej formie — to parametr runtime,
nie stala w kodzie. Bez tokenu generacja dziala jak dotad, z golymi
linkami (tryb lokalnego podgladu); wyjscie jest wtedy bajt w bajt takie
samo jak przed zmiana.

with_token() doklada ?key=... przed ewentualna kotwica i uzywa & gdy URL
ma juz wlasne query params (dzis nie ma — obrona na zapas). Token jedzie
przez urllib.parse.quote. Kotwice (#sekcja) i linki zewnetrzne zostaja
nietkniete. Wartosc nigdy nie leci na stdout — build() loguje tylko
TAK/NIE, bo logi z generacji bywaja wklejane.

--check: prawdziwy token (32+ hex) wygladal dla skanera dokladnie jak
wyciek `token-hex`. scan_line() wycina teraz wartosc `key=` WYLACZNIE
wewnatrz atrybutu href — ten sam token w tresci strony, po innym
parametrze niz key, albo poza href nadal jest raportowany jako wyciek.

Test: 11 stron public + index; z --access-token TEST123 wszystkie 12
linkow spisu, link doc->doc (agent-operating-procedures ->
action-approval-model) i kazdy powrot "All documents" niosa ?key=TEST123;
canonical swiadomie bez tokenu (metadana, nie nawigacja). Bez tokenu
diff vs HEAD pusty poza znacznikiem czasu. gen_pages --check exit 0 dla
generacji bez tokenu, z TEST123 i z realistycznym tokenem 64-hex;
check_okf.py exit 0.

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

View file

@ -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=<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.
```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=<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.
@ -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=<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
@ -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 |

View file

@ -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

View file

@ -25,6 +25,13 @@ Wyjście: build/kb-site/
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
w poszukiwaniu wycieków (adresy IP, porty, ścieżki hosta, tokeny). Świadome
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.
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]
"""
@ -43,11 +50,13 @@ from __future__ import annotations
import argparse
import html
import os
import posixpath
import re
import shutil
import subprocess
import sys
import urllib.parse
from dataclasses import dataclass
from datetime import datetime, timezone
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 ------------------------------------------------------------
@ -242,6 +288,9 @@ def render_inline(text: str) -> str:
attrs = ""
if href.startswith(("http://", "https://", "mailto:")):
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>'
text = _LINK_RE.sub(link, text)
@ -457,7 +506,8 @@ def page_shell(
) -> str:
up = "../" * depth
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 ""
return f"""<!doctype html>
<html lang="en">
@ -546,7 +596,7 @@ def render_index(docs: list[Doc], base_url: str, stamp: str) -> str:
for doc in items:
lead = summary(doc)
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'<span class="chip">{html.escape(doc.doc_type)}</span>'
+ (f"<p>{html.escape(lead)}</p>" if lead else "")
@ -589,7 +639,8 @@ def git_commit() -> str:
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()
public = [d for d in docs if d.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"Wyjście: {out_dir}")
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(
f"Dokumenty: {len(docs)} razem, {len(public)} public, "
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`.
_PORT_RE = re.compile(r"(?<!:):(\d{4,5})(?!\d)")
_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])")
_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]]:
# 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 += [("path-host", m.group(0)) for m in _PATH_RE.finditer(text)]
hits += _token_hits(text)
@ -830,6 +896,15 @@ def main() -> int:
default=DEFAULT_OUT,
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(
"--check",
action="store_true",
@ -846,7 +921,10 @@ def main() -> int:
if args.check:
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

View file

@ -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/<service>/ 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=<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).

View file

@ -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=<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