deploy: isolate git and connector secrets

This commit is contained in:
2026-08-04 15:04:39 +02:00
parent ff271d3cce
commit b5071f1494
7 changed files with 189 additions and 3 deletions
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Render optional secret overrides with disposable sources and enforce their isolation contract.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/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
docker compose --env-file "$fixture_root/.env" -f "$root/compose.yaml" "$@" config --format json \
>"$fixture_root/$name.json"
}
assert_render_contract() {
local name="$1" expected_targets="$2"
node - "$fixture_root/$name.json" "$name" "$expected_targets" \
"$fixture_root/ssh-private-key" "$fixture_root/ssh-known-hosts" \
"$fixture_root/https-credentials" "$fixture_root/https-ca.pem" \
"$fixture_root/dwh-password" "$fixture_root/vector-password" <<'NODE'
const fs = require("fs");
const [configPath, name, expectedTargets, ...sourcePaths] = 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 mountTargets = (core.volumes || [])
.filter((mount) => mount.target?.startsWith("/run/secrets/"))
.map((mount) => mount.target)
.sort();
const expectedMountTargets = expectedTargets ? expectedTargets.split(",").filter(Boolean).sort() : [];
if (mountTargets.join(",") !== expectedMountTargets.join(",")) {
throw new Error(`${name}: unexpected /run/secrets bind targets: ${mountTargets.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();
if (name === "connector" && secretTargets.join(",") !== "psd-clinical-dwh-password,psd-clinical-vector-password") {
throw new Error(`${name}: connector secret targets do not match declared THT_WS_*_FILE paths`);
}
if (name !== "connector" && secretTargets.length !== 0) {
throw new Error(`${name}: unexpected Docker 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") {
if (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-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-password"]) {
if (rendered.includes(secret)) throw new Error(`${name}: rendered Compose leaked fixture secret value`);
}
for (const sourcePath of sourcePaths) {
if (!sourcePath.startsWith("/")) throw new Error(`${name}: fixture source must be absolute`);
}
NODE
}
assert_required_source() {
local variable="$1" override="$2"
local missing_env="$fixture_root/missing-$variable.env"
grep -v "^$variable=" "$fixture_root/.env" >"$missing_env"
if env -u "$variable" docker compose --env-file "$missing_env" -f "$root/compose.yaml" -f "$override" config --quiet \
>"$fixture_root/missing-$variable.out" 2>"$fixture_root/missing-$variable.err"; then
echo "selected override accepted missing $variable" >&2
exit 1
fi
grep -Fq "$variable" "$fixture_root/missing-$variable.err"
}
write_secret "$fixture_root/pi-auth.json" 'fixture-pi-auth'
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-password" 'fixture-vector-password'
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
"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_PSD_CLINICAL_DWH_PASSWORD_SOURCE=$fixture_root/dwh-password" \
"THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_SOURCE=$fixture_root/vector-password" >"$fixture_root/.env"
render base
assert_render_contract base ''
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'
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'
render connector -f "$root/deploy/compose.connector-secrets.yaml"
assert_render_contract connector ''
assert_required_source THT_WORKSPACE_GIT_SSH_KEY_FILE "$root/deploy/compose.git-ssh.yaml"
assert_required_source THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE "$root/deploy/compose.git-ssh.yaml"
assert_required_source THT_WORKSPACE_GIT_CREDENTIALS_FILE "$root/deploy/compose.git-https.yaml"
assert_required_source THT_WORKSPACE_GIT_CA_FILE "$root/deploy/compose.git-https.yaml"
assert_required_source THT_WS_PSD_CLINICAL_DWH_PASSWORD_SOURCE "$root/deploy/compose.connector-secrets.yaml"
assert_required_source THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_SOURCE "$root/deploy/compose.connector-secrets.yaml"
echo "Compose secret policy passed."