202 lines
10 KiB
Bash
Executable File
202 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Render selected secret contracts through the real Compose preflight wrapper.
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
fixture_root="$(mktemp -d "${TMPDIR%/}/thoth-compose-secret-policy.XXXXXX")"
|
|
trap 'rm -rf "$fixture_root"' EXIT HUP INT TERM
|
|
|
|
write_secret() {
|
|
local path="$1" value="$2"
|
|
printf '%s' "$value" >"$path"
|
|
chmod 600 "$path"
|
|
}
|
|
|
|
render() {
|
|
local name="$1"; shift
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture_root/operator.env" \
|
|
-f "$root/compose.yaml" "$@" config --format json >"$fixture_root/$name.json"
|
|
}
|
|
|
|
assert_render_contract() {
|
|
local name="$1" expected_bind_targets="$2" expected_secret_targets="$3"
|
|
node - "$fixture_root/$name.json" "$name" "$expected_bind_targets" "$expected_secret_targets" <<'NODE'
|
|
const fs = require("fs");
|
|
|
|
const [configPath, name, expectedBindTargets, expectedSecretTargets] = process.argv.slice(2);
|
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
const core = config.services?.core;
|
|
if (!core) throw new Error(`${name}: missing core service`);
|
|
if (name === "base" && (core.volumes || []).some((mount) => mount.source === "/dev/null")) {
|
|
throw new Error("base: /dev/null must never be used as a secret mount source");
|
|
}
|
|
|
|
const bindTargets = (core.volumes || [])
|
|
.filter((mount) => mount.target?.startsWith("/run/secrets/"))
|
|
.map((mount) => mount.target)
|
|
.sort();
|
|
const expectedBinds = expectedBindTargets ? expectedBindTargets.split(",").filter(Boolean).sort() : [];
|
|
if (bindTargets.join(",") !== expectedBinds.join(",")) {
|
|
throw new Error(`${name}: unexpected /run/secrets bind targets: ${bindTargets.join(",")}`);
|
|
}
|
|
for (const mount of (core.volumes || []).filter((item) => item.target?.startsWith("/run/secrets/"))) {
|
|
if (mount.type !== "bind" || !mount.read_only) {
|
|
throw new Error(`${name}: secret bind ${mount.target} must be read-only`);
|
|
}
|
|
}
|
|
|
|
const secretTargets = (core.secrets || []).map((secret) => secret.target).sort();
|
|
const expectedSecrets = expectedSecretTargets ? expectedSecretTargets.split(",").filter(Boolean).sort() : [];
|
|
if (secretTargets.join(",") !== expectedSecrets.join(",")) {
|
|
throw new Error(`${name}: Docker secret targets do not match the deployment contract`);
|
|
}
|
|
if (core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
|
|
throw new Error(`${name}: core does not use the canonical /run/secrets bundle path`);
|
|
}
|
|
if ((config.services.frontend?.secrets || []).length !== 0) {
|
|
throw new Error(`${name}: frontend must not receive runtime secrets`);
|
|
}
|
|
|
|
if (name === "ssh") {
|
|
const command = core.environment?.GIT_SSH_COMMAND || "";
|
|
for (const option of ["IdentitiesOnly=yes", "StrictHostKeyChecking=yes", "UserKnownHostsFile=/run/secrets/workspace-registry-git-known-hosts"]) {
|
|
if (!command.includes(option)) throw new Error(`ssh: missing strict SSH option ${option}`);
|
|
}
|
|
}
|
|
if (name === "https" && core.environment?.GIT_CONFIG_VALUE_1 !== "/run/secrets/workspace-registry-git-ca") {
|
|
throw new Error("https: HTTPS CA verification is not configured");
|
|
}
|
|
|
|
const rendered = JSON.stringify(config);
|
|
for (const secret of ["fixture-model-api-key", "fixture-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-api-key"]) {
|
|
if (rendered.includes(secret)) throw new Error(`${name}: rendered Compose leaked fixture secret value`);
|
|
}
|
|
NODE
|
|
}
|
|
|
|
assert_missing_source_rejected() {
|
|
local variable="$1"
|
|
local generated="$fixture_root/missing-$variable.yaml"
|
|
local missing_env="$fixture_root/missing-$variable.env"
|
|
grep -v "^$variable=" "$fixture_root/operator.env" >"$missing_env"
|
|
if env -u "$variable" "$root/scripts/generate-connector-secrets-override.sh" \
|
|
--bindings-env "$fixture_root/workspace-bindings.env" --operator-env "$missing_env" --output "$generated" \
|
|
>"$fixture_root/missing-$variable.out" 2>"$fixture_root/missing-$variable.err"; then
|
|
echo "connector generator accepted missing $variable" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq "$variable" "$fixture_root/missing-$variable.err"
|
|
}
|
|
|
|
assert_unsafe_source_rejected() {
|
|
local value="$1" label="$2"
|
|
local unsafe_env="$fixture_root/$label.env"
|
|
sed "s|^THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=.*|THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$value|" \
|
|
"$fixture_root/operator.env" >"$unsafe_env"
|
|
if env -u THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE \
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$unsafe_env" -f "$root/compose.yaml" config --quiet \
|
|
>"$fixture_root/$label.out" 2>"$fixture_root/$label.err"; then
|
|
echo "Compose preflight accepted $label source path" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'unsafe source path' "$fixture_root/$label.err"
|
|
}
|
|
|
|
write_secret "$fixture_root/pi-auth.json" 'fixture-pi-auth'
|
|
write_secret "$fixture_root/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
|
|
write_secret "$fixture_root/ssh-private-key" 'fixture-ssh-private-key'
|
|
write_secret "$fixture_root/ssh-known-hosts" 'fixture-ssh-known-hosts'
|
|
write_secret "$fixture_root/https-credentials" 'fixture-https-credentials'
|
|
write_secret "$fixture_root/https-ca.pem" 'fixture-https-ca'
|
|
write_secret "$fixture_root/dwh-password" 'fixture-dwh-password'
|
|
write_secret "$fixture_root/vector-api-key" 'fixture-vector-api-key'
|
|
printf '%s\n' \
|
|
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
|
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
|
|
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
|
|
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture_root/workspace-bindings.env" \
|
|
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture_root/ssh-private-key" \
|
|
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture_root/ssh-known-hosts" \
|
|
"THT_WORKSPACE_GIT_CREDENTIALS_FILE=$fixture_root/https-credentials" \
|
|
"THT_WORKSPACE_GIT_CA_FILE=$fixture_root/https-ca.pem" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture_root/dwh-password" >"$fixture_root/operator.env"
|
|
|
|
printf '%s\n' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT=postgres_direct' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
|
|
>"$fixture_root/workspace-bindings.env"
|
|
|
|
connector_override="$fixture_root/compose.connector-secrets.local.yaml"
|
|
"$root/scripts/generate-connector-secrets-override.sh" \
|
|
--bindings-env "$fixture_root/workspace-bindings.env" --operator-env "$fixture_root/operator.env" \
|
|
--output "$connector_override"
|
|
|
|
render base
|
|
assert_render_contract base '' 'thothii.secrets'
|
|
render ssh -f "$root/deploy/compose.git-ssh.yaml"
|
|
assert_render_contract ssh '/run/secrets/workspace-registry-git-known-hosts,/run/secrets/workspace-registry-git-ssh-key' 'thothii.secrets'
|
|
render https -f "$root/deploy/compose.git-https.yaml"
|
|
assert_render_contract https '/run/secrets/workspace-registry-git-ca,/run/secrets/workspace-registry-git-credentials' 'thothii.secrets'
|
|
render connector -f "$connector_override"
|
|
assert_render_contract connector '' 'north-star-research-dwh-password,thothii.secrets'
|
|
|
|
assert_missing_source_rejected THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE
|
|
assert_unsafe_source_rejected 'relative/secret' relative-source
|
|
assert_unsafe_source_rejected '/private/secrets/../secret' non-normalized-source
|
|
if THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE='relative/host-override' \
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture_root/operator.env" -f "$root/compose.yaml" config --quiet \
|
|
>"$fixture_root/host-override.out" 2>"$fixture_root/host-override.err"; then
|
|
echo "Compose preflight accepted a relative exported source path" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'unsafe source path' "$fixture_root/host-override.err"
|
|
|
|
if THT_WS_HOST_ONLY_PASSWORD_SOURCE='relative/host-only' \
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture_root/operator.env" -f "$root/compose.yaml" config --quiet \
|
|
>"$fixture_root/host-only.out" 2>"$fixture_root/host-only.err"; then
|
|
echo "Compose preflight accepted a relative host-only source path" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'unsafe source path' "$fixture_root/host-only.err"
|
|
|
|
if "$root/scripts/compose-with-preflight.sh" --env-file "$fixture_root/operator.env" \
|
|
-f "$root/compose.yaml" -f "$root/deploy/compose.git-ssh.yaml" -f "$root/deploy/compose.git-https.yaml" config --quiet \
|
|
>"$fixture_root/combined.out" 2>"$fixture_root/combined.err"; then
|
|
echo "Compose preflight accepted combined SSH and HTTPS overrides" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'mutually exclusive' "$fixture_root/combined.err"
|
|
|
|
cp "$root/deploy/compose.git-ssh.yaml" "$fixture_root/transport-a.yaml"
|
|
cp "$root/deploy/compose.git-https.yaml" "$fixture_root/transport-b.yaml"
|
|
if "$root/scripts/compose-with-preflight.sh" --env-file "$fixture_root/operator.env" \
|
|
-f "$root/compose.yaml" -f "$fixture_root/transport-a.yaml" -f "$fixture_root/transport-b.yaml" config --quiet \
|
|
>"$fixture_root/renamed-combined.out" 2>"$fixture_root/renamed-combined.err"; then
|
|
echo "Compose preflight accepted renamed combined Git overrides" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'mutually exclusive' "$fixture_root/renamed-combined.err"
|
|
|
|
printf '%s\n' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE=/run/secrets/north-star-research-vector-api-key' \
|
|
>"$fixture_root/workspace-bindings-with-vector.env"
|
|
printf '%s\n' \
|
|
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
|
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
|
|
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture_root/dwh-password" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$fixture_root/vector-api-key" \
|
|
>"$fixture_root/operator-with-vector.env"
|
|
if "$root/scripts/generate-connector-secrets-override.sh" \
|
|
--bindings-env "$fixture_root/workspace-bindings-with-vector.env" \
|
|
--operator-env "$fixture_root/operator-with-vector.env" \
|
|
--output "$fixture_root/forbidden-vector.yaml" \
|
|
>"$fixture_root/forbidden-vector.out" 2>"$fixture_root/forbidden-vector.err"; then
|
|
echo "connector generator accepted a retired semantic API key binding" >&2
|
|
exit 1
|
|
fi
|
|
grep -Fq 'VECTOR_API_KEY_SOURCE' "$fixture_root/forbidden-vector.err"
|
|
|
|
echo "Compose secret policy passed."
|