docs: converge operator guidance on tht
This commit is contained in:
@@ -851,9 +851,9 @@ if (/\|\|\s*true|;\s*true\b/.test(updateShell)) throw new Error("POSIX source up
|
||||
requirePattern("POSIX source update does not fail closed: source pull", updateShell,
|
||||
/if ! git pull --ff-only; then abort_update/);
|
||||
requirePattern("POSIX source update does not fail closed: installation status", updateShell,
|
||||
/if ! INSTALLATION_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" status\)"; then/);
|
||||
/if ! INSTALLATION_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" status\)"; then/);
|
||||
requirePattern("POSIX source update does not fail closed: Pi status", updateShell,
|
||||
/if ! RUNNING_PI_VERSION="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/);
|
||||
/if ! RUNNING_PI_VERSION="\$\("\$THT_BIN" --installation "\$INSTALLATION" pi status\)"; then/);
|
||||
requirePattern("POSIX source update does not fail closed: local build", updateShell,
|
||||
/if ! bash scripts\/build-local\.sh; then/);
|
||||
requirePattern("POSIX source update does not fail closed: tht build", updateShell,
|
||||
@@ -861,12 +861,12 @@ requirePattern("POSIX source update does not fail closed: tht build", updateShel
|
||||
requirePattern("POSIX source update lacks the same-version/no-selector path", updateShell,
|
||||
/if \[\[ "\$NEXT_PI_VERSION" == "\$RUNNING_PI_VERSION" \]\]; then[\s\S]*"\$USES_BASE_CORE" == true[\s\S]*TRANSACTIONAL_PI_UPDATE=false/);
|
||||
for (const [label, pattern] of [
|
||||
["installation start", /if ! "\$THTCTL" --installation "\$INSTALLATION" start; then/],
|
||||
["installation start", /if ! "\$THT_BIN" --installation "\$INSTALLATION" start; then/],
|
||||
["frontend health", /if ! curl --fail http:\/\/127\.0\.0\.1:8080\/health; then/],
|
||||
["core health", /if ! curl --fail http:\/\/127\.0\.0\.1:8787\/health; then/],
|
||||
["final status", /if ! FINAL_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" status\)"; then/],
|
||||
["final Pi status", /if ! FINAL_PI_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/],
|
||||
["final doctor", /if ! "\$THTCTL" --installation "\$INSTALLATION" doctor; then/],
|
||||
["final status", /if ! FINAL_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" status\)"; then/],
|
||||
["final Pi status", /if ! FINAL_PI_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" pi status\)"; then/],
|
||||
["final doctor", /if ! "\$THT_BIN" --installation "\$INSTALLATION" doctor; then/],
|
||||
]) requirePattern(`POSIX source update does not fail closed: ${label}`, updateShell, pattern);
|
||||
const provenance = updateShell.indexOf("printf 'Built source revision:");
|
||||
if (provenance < updateShell.lastIndexOf("require_clean_source") ||
|
||||
@@ -883,14 +883,14 @@ requireTokens("native PowerShell source update", updatePowerShell, [
|
||||
]);
|
||||
for (const [command, step] of [
|
||||
["git pull --ff-only", "source pull"],
|
||||
["$InstallationStatus = @(& $THTCTL --installation $INSTALLATION status)", "installation status"],
|
||||
["$RunningPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "Pi status"],
|
||||
["$InstallationStatus = @(& $THT_BIN --installation $INSTALLATION status)", "installation status"],
|
||||
["$RunningPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)", "Pi status"],
|
||||
["powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1", "local image build"],
|
||||
["& \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build-tht.sh", "tht build"],
|
||||
["curl.exe --fail --silent --show-error http://127.0.0.1:8080/health", "frontend health check"],
|
||||
["curl.exe --fail --silent --show-error http://127.0.0.1:8787/health", "core health check"],
|
||||
["$FinalPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "final Pi status"],
|
||||
["& $THTCTL --installation $INSTALLATION doctor", "final doctor"],
|
||||
["$FinalPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)", "final Pi status"],
|
||||
["& $THT_BIN --installation $INSTALLATION doctor", "final doctor"],
|
||||
]) {
|
||||
const commandAt = updatePowerShell.indexOf(command);
|
||||
const checkAt = updatePowerShell.indexOf(`Assert-NativeSuccess '${step}'`, commandAt);
|
||||
@@ -963,7 +963,7 @@ NODE
|
||||
(
|
||||
cd "$update_fixture/project"
|
||||
env PATH="$update_fixture/bin:$PATH" CALLS="$calls" FAIL_STEP="$fixture_step" \
|
||||
THTCTL="$update_fixture/bin/tht" INSTALLATION="$update_fixture/installation.yaml" \
|
||||
THT_BIN="$update_fixture/bin/tht" INSTALLATION="$update_fixture/installation.yaml" \
|
||||
/bin/bash "$update_script"
|
||||
) >"$output" 2>&1
|
||||
status=$?
|
||||
@@ -1339,7 +1339,6 @@ verify_reverse_proxy_nginx_guide() {
|
||||
}
|
||||
require_headings "$guide" "Nginx reverse-proxy guide" \
|
||||
"Trust boundary" \
|
||||
"Example configuration" \
|
||||
"Validate and reload" \
|
||||
"Test authentication and SSE"
|
||||
require_text "$guide" "Nginx reverse-proxy guide" \
|
||||
@@ -1437,16 +1436,69 @@ function nginxLocations(text) {
|
||||
return locations;
|
||||
}
|
||||
const locations = nginxLocations(effectiveBlock);
|
||||
const authLocations = locations.filter((location) => location.selector === "= /_authenticate");
|
||||
if (authLocations.length !== 1) {
|
||||
throw new Error("Nginx proxy must define exactly one authentication location");
|
||||
}
|
||||
const authLocation = authLocations[0].body;
|
||||
const frontendLocations = locations.filter((location) =>
|
||||
/proxy_pass\s+http:\/\/127\.0\.0\.1:8080\s*;/.test(location.body));
|
||||
if (frontendLocations.length === 0) {
|
||||
throw new Error("Nginx proxy lacks a frontend upstream location");
|
||||
}
|
||||
const authenticatedFrontendLocations = frontendLocations.filter((location) =>
|
||||
/auth_request\s+\/_authenticate\s*;/.test(location.body));
|
||||
const directFrontendLocations = frontendLocations.filter((location) =>
|
||||
!/auth_request\s+\/_authenticate\s*;/.test(location.body));
|
||||
if (directFrontendLocations.length > 1) {
|
||||
throw new Error("Nginx direct OIDC mode contains an additional frontend bypass location");
|
||||
}
|
||||
for (const frontendLocation of frontendLocations) {
|
||||
for (const token of [
|
||||
"proxy_http_version 1.1;", "proxy_buffering off;", "proxy_cache off;",
|
||||
"proxy_read_timeout 3600s;",
|
||||
]) {
|
||||
if (!frontendLocation.body.includes(token)) {
|
||||
throw new Error(`Nginx proxy lacks structural token: ${token}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (authenticatedFrontendLocations.length > 0) {
|
||||
const authLocations = locations.filter((location) => location.selector === "= /_authenticate");
|
||||
if (authLocations.length !== 1) {
|
||||
throw new Error("Nginx proxy must define exactly one authentication location for upstream mode");
|
||||
}
|
||||
const authLocation = authLocations[0].body;
|
||||
for (const [label, publicName, variable, upstream] of identities) {
|
||||
const trustedName = publicName === "Is-Admin" ? "Is-Admin" : publicName;
|
||||
const publicClear = new RegExp(`proxy_set_header\\s+X-Thoth-${escaped(publicName)}\\s+"";`);
|
||||
const trustedHeader = `X-Thoth-Trusted-${trustedName}`;
|
||||
const trustedClear = new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+"";`);
|
||||
const authPublicAt = authLocation.search(publicClear);
|
||||
const authTrustedAt = authLocation.search(trustedClear);
|
||||
if (authPublicAt < 0) {
|
||||
throw new Error(`Nginx auth location does not clear inbound ${label} identity`);
|
||||
}
|
||||
if (authTrustedAt < 0) {
|
||||
throw new Error(`Nginx auth location does not clear inbound trusted ${label} identity`);
|
||||
}
|
||||
for (const frontendLocation of authenticatedFrontendLocations) {
|
||||
const frontendPublicAt = frontendLocation.body.search(publicClear);
|
||||
if (frontendPublicAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not clear inbound ${label} identity`);
|
||||
}
|
||||
const normalizedFrontend = frontendLocation.body.replace(/\s+/g, " ");
|
||||
const captureAt = normalizedFrontend.search(new RegExp(`auth_request_set\\s+\\$${variable}\\s+\\$upstream_http_${upstream};`));
|
||||
if (captureAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not capture authenticated ${label} identity`);
|
||||
}
|
||||
const mapAt = normalizedFrontend.search(new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`));
|
||||
if (mapAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not map authenticated ${label} identity`);
|
||||
}
|
||||
}
|
||||
if (new RegExp(`auth_request_set\\s+\\$${variable}|proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`).test(authLocation)) {
|
||||
throw new Error(`Nginx auth location performs a forbidden ${label} capture or mapping`);
|
||||
}
|
||||
}
|
||||
} else if (directFrontendLocations.length === 0) {
|
||||
throw new Error("Nginx proxy lacks a direct or authenticated frontend path");
|
||||
}
|
||||
for (const location of locations) {
|
||||
const upstreams = [...location.body.matchAll(/proxy_pass\s+([^;]+);/g)].map((match) => match[1].trim());
|
||||
for (const upstream of upstreams) {
|
||||
@@ -1455,41 +1507,9 @@ for (const location of locations) {
|
||||
throw new Error(`Nginx location proxies to an unreviewed upstream: ${upstream}`);
|
||||
}
|
||||
}
|
||||
for (const frontendLocation of frontendLocations) {
|
||||
for (const frontendLocation of authenticatedFrontendLocations) {
|
||||
if (!/auth_request\s+\/_authenticate\s*;/.test(frontendLocation.body)) {
|
||||
throw new Error("Nginx frontend upstream location bypasses complete authentication contract");
|
||||
}
|
||||
}
|
||||
for (const [label, publicName, variable, upstream] of identities) {
|
||||
const trustedName = publicName === "Is-Admin" ? "Is-Admin" : publicName;
|
||||
const publicClear = new RegExp(`proxy_set_header\\s+X-Thoth-${escaped(publicName)}\\s+"";`);
|
||||
const trustedHeader = `X-Thoth-Trusted-${trustedName}`;
|
||||
const trustedClear = new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+"";`);
|
||||
const authPublicAt = authLocation.search(publicClear);
|
||||
const authTrustedAt = authLocation.search(trustedClear);
|
||||
if (authPublicAt < 0) {
|
||||
throw new Error(`Nginx auth location does not clear inbound ${label} identity`);
|
||||
}
|
||||
if (authTrustedAt < 0) {
|
||||
throw new Error(`Nginx auth location does not clear inbound trusted ${label} identity`);
|
||||
}
|
||||
for (const frontendLocation of frontendLocations) {
|
||||
const frontendPublicAt = frontendLocation.body.search(publicClear);
|
||||
if (frontendPublicAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not clear inbound ${label} identity`);
|
||||
}
|
||||
const normalizedFrontend = frontendLocation.body.replace(/\s+/g, " ");
|
||||
const captureAt = normalizedFrontend.search(new RegExp(`auth_request_set\\s+\\$${variable}\\s+\\$upstream_http_${upstream};`));
|
||||
if (captureAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not capture authenticated ${label} identity`);
|
||||
}
|
||||
const mapAt = normalizedFrontend.search(new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`));
|
||||
if (mapAt < 0) {
|
||||
throw new Error(`Nginx frontend location does not map authenticated ${label} identity`);
|
||||
}
|
||||
}
|
||||
if (new RegExp(`auth_request_set\\s+\\$${variable}|proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`).test(authLocation)) {
|
||||
throw new Error(`Nginx auth location performs a forbidden ${label} capture or mapping`);
|
||||
throw new Error("Nginx authenticated frontend path bypasses complete authentication contract");
|
||||
}
|
||||
}
|
||||
NODE
|
||||
@@ -1504,21 +1524,20 @@ verify_reverse_proxy_caddy_guide() {
|
||||
}
|
||||
require_headings "$guide" "Caddy reverse-proxy guide" \
|
||||
"Trust boundary" \
|
||||
"Example configuration" \
|
||||
"Validate and reload" \
|
||||
"Test authentication and SSE"
|
||||
require_text "$guide" "Caddy reverse-proxy guide" \
|
||||
"Forwarding identity headers alone does not authenticate a user" \
|
||||
"authentication gateway" \
|
||||
"2xx" \
|
||||
"automatic HTTPS" \
|
||||
"Caddy terminates TLS" \
|
||||
"frontend"
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const source = fs.readFileSync(process.argv[2], "utf8");
|
||||
const block = [...source.matchAll(/```caddyfile\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
|
||||
const tokens = [
|
||||
"thoth.example.com {", "route {",
|
||||
"thoth.example.invalid {", "route {",
|
||||
"forward_auth auth-gateway:4180 {", "uri /verify", "copy_headers {",
|
||||
"reverse_proxy 127.0.0.1:8080 {", "flush_interval -1",
|
||||
];
|
||||
@@ -1730,15 +1749,15 @@ verify_manual() {
|
||||
'export THT_SOURCE_ROOT=/absolute/path/to/ThothII'
|
||||
'thothii-installation.yaml'
|
||||
'workspaceRepository'
|
||||
'"$THTCTL" --installation "$INSTALLATION" start'
|
||||
'"$THTCTL" --installation "$INSTALLATION" doctor'
|
||||
'"$THT_BIN" --installation "$INSTALLATION" start'
|
||||
'"$THT_BIN" --installation "$INSTALLATION" doctor'
|
||||
)
|
||||
else
|
||||
expected_steps=(
|
||||
'THTCTL=/srv/thothii/operator/tht'
|
||||
'THT_BIN=/srv/thothii/operator/tht'
|
||||
'INSTALLATION=/srv/thothii/operator/thothii-installation.yaml'
|
||||
'"$THTCTL" --installation "$INSTALLATION" start'
|
||||
'"$THTCTL" --installation "$INSTALLATION" doctor'
|
||||
'"$THT_BIN" --installation "$INSTALLATION" start'
|
||||
'"$THT_BIN" --installation "$INSTALLATION" doctor'
|
||||
'docs/install/examples/thothii-installation.server.yaml'
|
||||
'compose.yaml'
|
||||
'deploy/compose.server.yaml'
|
||||
@@ -1829,7 +1848,7 @@ for required in (
|
||||
|
||||
ui = (root / "frontend/src/shell/WorkspaceManager.tsx").read_text()
|
||||
for required in (
|
||||
"Create a local workspace",
|
||||
"Create a workspace repository",
|
||||
"Update workspace repository",
|
||||
"No workspace selection is required",
|
||||
"Temporary files are deleted after the test",
|
||||
@@ -2024,8 +2043,9 @@ if (Object.keys(config.services).sort().join(",") !== "core,embedding,embedding-
|
||||
}
|
||||
const core = config.services.core;
|
||||
const frontend = config.services.frontend;
|
||||
if (core.environment?.AUTH_MODE !== "upstream" || core.environment?.THOTH_PUBLIC_EXPOSURE !== "true") {
|
||||
throw new Error("server installation example must fail closed behind upstream authentication");
|
||||
if (core.environment?.THOTH_PUBLIC_EXPOSURE !== "true" ||
|
||||
core.environment?.THT_AUTH_CONFIG_FILE !== "/run/thothii-auth/auth.yaml") {
|
||||
throw new Error("server installation example must expose only the authenticated frontend");
|
||||
}
|
||||
if ((core.ports || []).length !== 0) throw new Error("server installation example published core");
|
||||
const ports = frontend.ports || [];
|
||||
|
||||
Reference in New Issue
Block a user