deploy: isolate git and connector secrets
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# Selected direct PostgreSQL/pgvector connector secrets. Each target must match a corresponding
|
||||||
|
# THT_WS_*_FILE=/run/secrets/<target> binding; source paths are host-only operator configuration.
|
||||||
|
services:
|
||||||
|
core:
|
||||||
|
secrets:
|
||||||
|
- source: psd_clinical_dwh_password
|
||||||
|
target: psd-clinical-dwh-password
|
||||||
|
- source: psd_clinical_vector_password
|
||||||
|
target: psd-clinical-vector-password
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
psd_clinical_dwh_password:
|
||||||
|
file: ${THT_WS_PSD_CLINICAL_DWH_PASSWORD_SOURCE:?set THT_WS_PSD_CLINICAL_DWH_PASSWORD_SOURCE}
|
||||||
|
psd_clinical_vector_password:
|
||||||
|
file: ${THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_SOURCE:?set THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_SOURCE}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Select this override only for an HTTPS Git remote. The separate CA mount keeps TLS validation
|
||||||
|
# explicit; neither host-only source file nor its contents belongs in the base Compose contract.
|
||||||
|
services:
|
||||||
|
core:
|
||||||
|
environment:
|
||||||
|
GIT_CONFIG_COUNT: "2"
|
||||||
|
GIT_CONFIG_KEY_0: credential.helper
|
||||||
|
GIT_CONFIG_VALUE_0: store --file=/run/secrets/workspace-registry-git-credentials
|
||||||
|
GIT_CONFIG_KEY_1: http.sslCAInfo
|
||||||
|
GIT_CONFIG_VALUE_1: /run/secrets/workspace-registry-git-ca
|
||||||
|
volumes:
|
||||||
|
- ${THT_WORKSPACE_GIT_CREDENTIALS_FILE:?set THT_WORKSPACE_GIT_CREDENTIALS_FILE}:/run/secrets/workspace-registry-git-credentials:ro
|
||||||
|
- ${THT_WORKSPACE_GIT_CA_FILE:?set THT_WORKSPACE_GIT_CA_FILE}:/run/secrets/workspace-registry-git-ca:ro
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Select this override only for an SSH Git remote. The host-only source files must be absolute,
|
||||||
|
# normalized paths; strict host-key checking is mandatory for registry pull and publish.
|
||||||
|
services:
|
||||||
|
core:
|
||||||
|
environment:
|
||||||
|
GIT_SSH_COMMAND: ssh -i /run/secrets/workspace-registry-git-ssh-key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/run/secrets/workspace-registry-git-known-hosts
|
||||||
|
volumes:
|
||||||
|
- ${THT_WORKSPACE_GIT_SSH_KEY_FILE:?set THT_WORKSPACE_GIT_SSH_KEY_FILE}:/run/secrets/workspace-registry-git-ssh-key:ro
|
||||||
|
- ${THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE:?set THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE}:/run/secrets/workspace-registry-git-known-hosts:ro
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copy these non-secret registry settings into the installation environment.
|
# Copy these non-secret registry settings into the installation environment.
|
||||||
# Create the referenced credential, CA, SSH-key, and known-hosts files locally with restrictive
|
# Select at most one Git transport override and only the connector-secret entries whose matching
|
||||||
# permissions. Their contents are never committed, emitted by the API, or stored in the registry.
|
# THT_WS_*_FILE bindings are declared. Every host path below must be absolute and normalized.
|
||||||
|
# Their contents are never committed, emitted by the API, or stored in the registry.
|
||||||
THT_WORKSPACE_REGISTRY_ROOT=/data/workspace-registry
|
THT_WORKSPACE_REGISTRY_ROOT=/data/workspace-registry
|
||||||
THT_WORKSPACE_GIT_BRANCH=main
|
THT_WORKSPACE_GIT_BRANCH=main
|
||||||
THT_WORKSPACE_INSTALLATION_ID=local
|
THT_WORKSPACE_INSTALLATION_ID=local
|
||||||
@@ -13,3 +14,8 @@ THT_WORKSPACE_GIT_AUTHOR_EMAIL=thoth-workspace-registry@localhost
|
|||||||
# THT_WORKSPACE_GIT_CA_FILE=/absolute/path/to/git-ca.pem
|
# THT_WORKSPACE_GIT_CA_FILE=/absolute/path/to/git-ca.pem
|
||||||
# THT_WORKSPACE_GIT_SSH_KEY_FILE=/absolute/path/to/git-ssh-key
|
# THT_WORKSPACE_GIT_SSH_KEY_FILE=/absolute/path/to/git-ssh-key
|
||||||
# THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=/absolute/path/to/git-known-hosts
|
# THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=/absolute/path/to/git-known-hosts
|
||||||
|
|
||||||
|
# Host-only connector sources consumed only by deploy/compose.connector-secrets.yaml. The matching
|
||||||
|
# THT_WS_*_FILE values belong in the separate workspace bindings env file and target /run/secrets.
|
||||||
|
# THT_WS_PSD_CLINICAL_DWH_PASSWORD_SOURCE=/absolute/path/to/psd-clinical-dwh-password
|
||||||
|
# THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_SOURCE=/absolute/path/to/psd-clinical-vector-password
|
||||||
|
|||||||
Executable
+127
@@ -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."
|
||||||
@@ -16,7 +16,9 @@ for fixture in \
|
|||||||
"copied connector binding/secret fixture" \
|
"copied connector binding/secret fixture" \
|
||||||
"core process sees connector bindings and secret files" \
|
"core process sees connector bindings and secret files" \
|
||||||
"non-path secret-file fixture rejected" \
|
"non-path secret-file fixture rejected" \
|
||||||
"literal secret-source fixture rejected"; do
|
"literal secret-source fixture rejected" \
|
||||||
|
"relative secret-source fixture rejected" \
|
||||||
|
"non-normalized secret-source fixture rejected"; do
|
||||||
grep -Fqx "$fixture passed" "$output" >/dev/null || {
|
grep -Fqx "$fixture passed" "$output" >/dev/null || {
|
||||||
echo "missing fixture verification: $fixture" >&2
|
echo "missing fixture verification: $fixture" >&2
|
||||||
cat "$output" >&2
|
cat "$output" >&2
|
||||||
|
|||||||
@@ -190,6 +190,20 @@ verify_copied_operator_fixtures() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "literal secret-source fixture rejected passed"
|
echo "literal secret-source fixture rejected passed"
|
||||||
|
|
||||||
|
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=installation-secrets/password\n' >"$fixture_root/relative-source.env"
|
||||||
|
if verify_path_variable_values "$fixture_root/relative-source.env" >/dev/null 2>&1; then
|
||||||
|
echo "relative secret-source fixture was accepted" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "relative secret-source fixture rejected passed"
|
||||||
|
|
||||||
|
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=/srv/thothii/secrets/../password\n' >"$fixture_root/non-normalized-source.env"
|
||||||
|
if verify_path_variable_values "$fixture_root/non-normalized-source.env" >/dev/null 2>&1; then
|
||||||
|
echo "non-normalized secret-source fixture was accepted" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "non-normalized secret-source fixture rejected passed"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$profile" in
|
case "$profile" in
|
||||||
|
|||||||
Reference in New Issue
Block a user