feat(server): activate projected authentication safely
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
tmp_base="${TMPDIR:-/tmp}"
|
||||
tmp="$(mktemp -d "${tmp_base%/}/thoth-auth-runtime-compose.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
canonical="$tmp/canonical-auth"
|
||||
runtime="$tmp/runtime-auth"
|
||||
mkdir -p "$canonical" "$runtime" "$tmp/data" "$tmp/pi-state" "$tmp/workspace-registry"
|
||||
chmod 0700 "$canonical" "$runtime"
|
||||
printf '%s\n' '{}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' 'fixture-secret-sentinel' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
|
||||
|
||||
write_env() {
|
||||
local path="$1"
|
||||
{
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/workspaces.git' \
|
||||
"PI_AUTH_FILE=$tmp/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$tmp/thothii.secrets" \
|
||||
"THT_AUTH_CONFIG_ROOT=$canonical" \
|
||||
"THT_DATA_ROOT=$tmp/data" \
|
||||
"THT_PI_STATE_ROOT=$tmp/pi-state" \
|
||||
"THT_WORKSPACE_REGISTRY_ROOT=$tmp/workspace-registry"
|
||||
} >"$path"
|
||||
}
|
||||
|
||||
write_env "$tmp/nonprojected.env"
|
||||
cp "$tmp/nonprojected.env" "$tmp/projected.env"
|
||||
printf 'THT_AUTH_RUNTIME_ROOT=%s\n' "$runtime" >>"$tmp/projected.env"
|
||||
|
||||
cat >"$tmp/operator.yaml" <<'YAML'
|
||||
services:
|
||||
core:
|
||||
environment:
|
||||
THT_AUTH_RUNTIME_PROJECTION_ROOT: operator-marker
|
||||
YAML
|
||||
|
||||
cat >"$tmp/current-image.yaml" <<'YAML'
|
||||
services:
|
||||
core:
|
||||
environment:
|
||||
THT_AUTH_RUNTIME_PROJECTION_ROOT: current-marker
|
||||
YAML
|
||||
|
||||
render() {
|
||||
local output="$1"
|
||||
local env_file="$2"
|
||||
shift 2
|
||||
local -a files=(-f "$root/compose.yaml" -f "$root/deploy/compose.server.yaml")
|
||||
local file
|
||||
for file in "$@"; do
|
||||
files+=(-f "$file")
|
||||
done
|
||||
docker compose --project-directory "$root" --env-file "$env_file" "${files[@]}" \
|
||||
config --format json >"$output"
|
||||
}
|
||||
|
||||
render "$tmp/nonprojected.json" "$tmp/nonprojected.env"
|
||||
render "$tmp/projected.json" "$tmp/projected.env" \
|
||||
"$tmp/operator.yaml" "$root/deploy/compose.auth-runtime-projection.yaml"
|
||||
render "$tmp/current.json" "$tmp/projected.env" \
|
||||
"$tmp/operator.yaml" "$root/deploy/compose.auth-runtime-projection.yaml" \
|
||||
"$tmp/current-image.yaml"
|
||||
|
||||
for mode in nonprojected projected current; do
|
||||
node - "$tmp/$mode.json" "$mode" "$canonical" "$runtime" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const [path, mode, canonical, runtime] = process.argv.slice(2);
|
||||
const config = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
const core = config.services?.core;
|
||||
if (!core) throw new Error(`${mode}: missing core service`);
|
||||
|
||||
const mounts = (core.volumes || []).filter((mount) => mount.target === "/run/thothii-auth");
|
||||
if (mounts.length !== 1 || mounts[0].type !== "bind" || !mounts[0].read_only) {
|
||||
throw new Error(`${mode}: expected exactly one read-only auth bind`);
|
||||
}
|
||||
if (mode === "nonprojected") {
|
||||
if (mounts[0].source !== canonical) throw new Error("nonprojected: canonical auth source changed");
|
||||
if (Object.hasOwn(core.environment || {}, "THT_AUTH_RUNTIME_PROJECTION_ROOT")) {
|
||||
throw new Error("nonprojected: projected environment unexpectedly present");
|
||||
}
|
||||
} else {
|
||||
if (mounts[0].source !== runtime) throw new Error(`${mode}: runtime source did not replace canonical source`);
|
||||
if ((core.volumes || []).some((mount) => mount.source === canonical)) {
|
||||
throw new Error(`${mode}: canonical source is still mounted`);
|
||||
}
|
||||
const expected = mode === "projected" ? "/run/thothii-auth" : "current-marker";
|
||||
if (core.environment?.THT_AUTH_RUNTIME_PROJECTION_ROOT !== expected) {
|
||||
throw new Error(`${mode}: override ordering failed`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, service] of Object.entries(config.services || {})) {
|
||||
if (name !== "core" && Object.hasOwn(service.environment || {}, "THT_AUTH_RUNTIME_PROJECTION_ROOT")) {
|
||||
throw new Error(`${mode}: ${name} received projected auth environment`);
|
||||
}
|
||||
}
|
||||
const maintenance = config.services?.["workspace-maintenance"];
|
||||
if ((maintenance?.volumes || []).some(
|
||||
(mount) => mount.target === "/run/thothii-auth" || mount.target === "/data/auth",
|
||||
)) {
|
||||
throw new Error(`${mode}: workspace-maintenance received auth mount`);
|
||||
}
|
||||
if (Object.keys(maintenance?.environment || {}).some((key) => key.startsWith("THT_AUTH_"))) {
|
||||
throw new Error(`${mode}: workspace-maintenance received auth environment`);
|
||||
}
|
||||
if (JSON.stringify(config).includes("fixture-secret-sentinel")) {
|
||||
throw new Error(`${mode}: rendered Compose leaked a secret sentinel`);
|
||||
}
|
||||
NODE
|
||||
done
|
||||
|
||||
echo "runtime auth projection Compose contract passed."
|
||||
@@ -79,6 +79,9 @@ const authConfig = config.services.core.volumes?.filter((mount) => mount.target
|
||||
if (authConfig.length !== 1 || authConfig[0].type !== "bind" || !authConfig[0].read_only) {
|
||||
throw new Error(profile + ": core must receive one read-only authentication config bind");
|
||||
}
|
||||
if (Object.hasOwn(config.services.core.environment || {}, "THT_AUTH_RUNTIME_PROJECTION_ROOT")) {
|
||||
throw new Error(profile + ": non-projected fixture unexpectedly selected runtime projection");
|
||||
}
|
||||
if (profile === "local") {
|
||||
const authState = config.services.core.volumes?.filter((mount) => mount.target === "/data/auth") || [];
|
||||
if (authState.length !== 1 || authState[0].type !== "volume" || authState[0].source !== "auth-state") {
|
||||
|
||||
Reference in New Issue
Block a user