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`.
|
||||
- `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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue