fix: harden DWH auth recovery guidance
This commit is contained in:
@@ -61,6 +61,57 @@ expect_rejected() {
|
||||
report_pass "$name"
|
||||
}
|
||||
|
||||
exercise_header_file_curl() {
|
||||
local v1_header="$temp_root/synthetic-v1.header"
|
||||
local legacy_header="$temp_root/synthetic-legacy.header"
|
||||
command -v curl >/dev/null 2>&1 || report_fail header_file_curl_command
|
||||
printf '%s\n' 'X-API-Key: synthetic-v1' >"$v1_header"
|
||||
printf '%s\n' 'X-API-Key: legacy opaque value /:[]' >"$legacy_header"
|
||||
chmod 0600 "$v1_header" "$legacy_header"
|
||||
if ! python3 - "$v1_header" "$legacy_header" <<'PY'
|
||||
import http.server
|
||||
import pathlib
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
accepted = {"synthetic-v1", "legacy opaque value /:[]"}
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, _format, *_args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(204 if self.headers.get("X-API-Key") in accepted else 401)
|
||||
self.end_headers()
|
||||
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
for header_file in sys.argv[1:]:
|
||||
if stat.S_IMODE(pathlib.Path(header_file).stat().st_mode) != 0o600:
|
||||
raise SystemExit(1)
|
||||
result = subprocess.run(
|
||||
["curl", "--silent", "--show-error", "--output", "/dev/null", "--write-out", "%{http_code}", "--header", f"@{header_file}", f"http://127.0.0.1:{server.server_port}/rpc/ping"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0 or result.stdout != "204":
|
||||
raise SystemExit(1)
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
PY
|
||||
then
|
||||
report_fail header_file_curl_synthetic
|
||||
fi
|
||||
report_pass header_file_curl_synthetic
|
||||
}
|
||||
|
||||
# Build synthetic only-in-fixture text at runtime: it is never a provisioned credential.
|
||||
fake_key="thtdwh_v1.$(printf 'A%.0s' {1..16}).$(printf 'A%.0s' {1..43})"
|
||||
fake_digest="$(printf 'A%.0s' {1..43})"
|
||||
@@ -72,6 +123,7 @@ expect_rejected curl_insecure docs/install/dwh-auth-tls.md 'curl -k https://exam
|
||||
expect_rejected tls_disabled docs/install/dwh-auth-tls.md 'verify_tls=false'
|
||||
expect_rejected secret_in_environment docs/install/dwh-auth-client-enrollment.md "export THT_WS_PSD_CLINICAL_DWH_API_KEY=$fake_key"
|
||||
expect_rejected secret_in_argv docs/install/dwh-auth-client-enrollment.md "curl -H 'X-API-Key: $fake_key' https://example.invalid/dwh/rpc/ping"
|
||||
expect_rejected curl_config_artifact docs/install/dwh-auth-server.md 'curl --config /root/dwh-auth-provision/request.conf https://example.invalid/dwh/rpc/ping'
|
||||
expect_rejected world_readable_secret docs/install/dwh-auth-server.md 'chmod 0644 /root/dwh-auth-provision/client.key'
|
||||
expect_rejected sudo_raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'sudo nginx -T > /tmp/nginx-full.conf'
|
||||
expect_rejected raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'sudo diff -u /etc/nginx/sites-available/policlinicosandonato /root/backup.conf'
|
||||
@@ -100,6 +152,8 @@ expect_global_replacement_rejected() {
|
||||
report_pass "$name"
|
||||
}
|
||||
|
||||
exercise_header_file_curl
|
||||
|
||||
expect_replacement_rejected missing_exact_gui_label docs/install/dwh-auth-client-enrollment.md "Validate workspace source" "Validate workspace"
|
||||
expect_replacement_rejected server_transport_contradiction docs/operations/psd-dwh-auth-rollout.md 'server PSD: `postgres_direct` read-only' 'server PSD: `rest_api` read-only'
|
||||
expect_replacement_rejected missing_mac_local_marker deploy/psd/workspace-bindings.env.example "Mac/local/remota" "server PSD"
|
||||
@@ -107,4 +161,27 @@ expect_replacement_rejected missing_private_ca docs/install/psd-workspace-setup.
|
||||
expect_global_replacement_rejected missing_socket_path docs/install/dwh-auth-server.md "/run/dwh-auth/verify.sock" "/run/dwh-auth/other.sock"
|
||||
expect_replacement_rejected rest_transport_flag deploy/psd/workspace-bindings.env.example "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api" "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct"
|
||||
|
||||
expect_replacement_rejected wrong_key_output_path docs/install/dwh-auth-server.md "key_output=/root/dwh-auth-provision/psd-mac-primary.key" "key_output=/root/dwh-auth-provision/psd-mac-primary-primary.key"
|
||||
expect_replacement_rejected wrong_legacy_key_id docs/install/dwh-auth-server.md "legacy_key_id=legacy-shared" "legacy_key_id=legacy-public-key-id"
|
||||
expect_global_replacement_rejected missing_header_file_transport docs/install/dwh-auth-server.md '--header "@' '--header "X-API-Key: '
|
||||
expect_global_replacement_rejected missing_legacy_header_file docs/install/dwh-auth-server.md "legacy_header_file" "retired_auth_header"
|
||||
expect_global_replacement_rejected missing_safe_restore docs/install/dwh-auth-server.md "registry_staging" "registry_overlay"
|
||||
expect_replacement_rejected unsafe_restore_overlay docs/install/dwh-auth-server.md "sudo tar --acls --xattrs -C \"\$registry_staging\" -xf \"\$registry_backup\"" "sudo tar --acls --xattrs -C /var/lib -xf \"\$registry_backup\""
|
||||
expect_replacement_rejected missing_guarded_registry_publish docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "sudo mv -T -- \"\$registry_root\" \"\$registry_previous\""
|
||||
expect_replacement_rejected missing_candidate_check_guard docs/install/dwh-auth-server.md "if ! sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check; then" "sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check"
|
||||
expect_replacement_rejected unsafe_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_previous\"; then"
|
||||
expect_replacement_rejected missing_key_file_byte_guard docs/install/dwh-auth-server.md "key_file_bytes=PASS" "key_file_bytes=SKIPPED"
|
||||
expect_replacement_rejected missing_failed_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then"
|
||||
expect_global_replacement_rejected missing_rollback_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_previous\" \"\$registry_root\"; then" "if ! sudo mv \"\$registry_previous\" \"\$registry_root\"; then"
|
||||
expect_global_replacement_rejected missing_exact_header_bytes docs/install/dwh-auth-server.md 'dd if="$1" bs=65536 status=none' 'dd if="$1" bs=1'
|
||||
|
||||
|
||||
expect_replacement_rejected missing_actual_key_journal_scan docs/install/dwh-auth-server.md "journal_actual_key_scan=PASS" "journal_generic_scan=PASS"
|
||||
expect_replacement_rejected missing_journal_actual_key_match docs/install/dwh-auth-server.md "any(key in line for key in actual_keys)" "False"
|
||||
expect_replacement_rejected missing_journal_returncode_guard docs/install/dwh-auth-server.md "if result.returncode != 0:" "if result.returncode == 0:"
|
||||
expect_replacement_rejected missing_socket_legacy_probe docs/install/dwh-auth-server.md "socket_legacy=PASS" "socket_legacy=SKIPPED"
|
||||
expect_replacement_rejected missing_https_post_revoke_v1 docs/install/dwh-auth-server.md "https_v1_post_revoke=PASS" "https_v1_post_revoke=SKIPPED"
|
||||
expect_replacement_rejected missing_https_post_revoke_legacy docs/install/dwh-auth-server.md "https_legacy_post_revoke=PASS" "https_legacy_post_revoke=SKIPPED"
|
||||
expect_global_replacement_rejected rollout_post_revoke_status docs/operations/psd-dwh-auth-rollout.md "v1=2xx post-revoca" "v1=204 post-revoca"
|
||||
|
||||
report_pass summary
|
||||
|
||||
@@ -34,10 +34,10 @@ for label, relative in docs.items():
|
||||
text[label] = path.read_text(encoding="utf-8")
|
||||
|
||||
requirements = {
|
||||
"server": ["manifest", "curl_cfg", "journalctl", "systemctl disable --now", "trap", "/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"],
|
||||
"server": ["key_output=/root/dwh-auth-provision/psd-mac-primary.key", "legacy_key_id=legacy-shared", "v1_header_file", "legacy_header_file", "random_header_file", "--header \"@", "registry_staging", "registry_previous", "registry_restore_rollback", "journal_actual_key_scan=PASS", "sys.argv[2:]", "read_bytes()", "subprocess.run", "stderr=subprocess.DEVNULL", "returncode != 0", "socket_v1=PASS", "socket_legacy=PASS", "https_v1_post_revoke=PASS", "https_legacy_post_revoke=PASS", "key_file_bytes=PASS", "dd if=\"$1\" bs=65536 status=none", "manifest", "journalctl", "systemctl disable --now", "trap", "/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"],
|
||||
"client": ["Workspace management", "Validate workspace source", "Test workspace connections", "Save entered secrets", "Forget stored value", "API_KEY_FILE", "THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "rest_api", "postgres_direct", "ssh_tunnel", "401", "503", "rotazione", "revoca"],
|
||||
"tls": ["self-issued", ".it", ".com", "SAN", "TLS_CA_FILE", "openssl x509 -noout -fingerprint -sha256", "fuori banda", "rinnovo", "curl -k"],
|
||||
"rollout": ["PASS/FAIL", "no raw diff", "Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"],
|
||||
"rollout": ["v1=2xx", "legacy=2xx", "v1=2xx post-revoca", "legacy=401 post-revoca", "file header curl protetti 0600", "PASS/FAIL", "no raw diff", "Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"],
|
||||
"manual": ["credenziali reali", "sintetici", "/rpc/ping", "204", "401", "503", "TLS", "registry", "postgres_direct", "ssh_tunnel"],
|
||||
"evidence": ["ID pubblici", "owner", "mode", "timestamp", "checksum", "approvazione"],
|
||||
}
|
||||
@@ -72,6 +72,7 @@ for pattern, label in [
|
||||
(r"(?i)chmod\s+0?[0-7][0-7][4-7]\s+[^\n]*(?:\.key|secret|provision)", "world-readable secret"),
|
||||
(r"(?m)^\s*(?:sudo\s+)?nginx\s+-T\b", "raw Nginx capture"),
|
||||
(r"(?m)^\s*(?:sudo\s+)?(?:diff\b|git\s+diff\b)", "raw diff capture"),
|
||||
(r"(?m)^\s*(?:sudo\s+)?curl[^\n]*--config\b", "curl config artifact"),
|
||||
(r"(?i)docker\s+compose[^\n]*\bdwh-auth\b", "Compose coupling"),
|
||||
]:
|
||||
if re.search(pattern, corpus):
|
||||
@@ -87,5 +88,36 @@ if "tht_ws_psd_clinical_dwh_transport=rest_api" not in template.lower() or "mac/
|
||||
if "TLS_CA_FILE" not in text["psd"] or re.search(r"(?i)nessuna CA|HTTPS pubblico", text["psd"]):
|
||||
raise SystemExit("dwh-auth docs: PSD setup contradicts private CA TLS requirement")
|
||||
|
||||
restore_steps = (
|
||||
'registry_staging="/var/lib/.dwh-auth-restore-$run_id"',
|
||||
'registry_candidate="$registry_staging/dwh-auth"',
|
||||
'sudo tar --acls --xattrs -C "$registry_staging" -xf "$registry_backup"',
|
||||
'if ! sudo /usr/local/sbin/dwh-auth --registry-root "$registry_candidate" check; then',
|
||||
'if ! sudo mv -T -- "$registry_root" "$registry_previous"; then',
|
||||
'if ! sudo mv -T -- "$registry_candidate" "$registry_root"; then',
|
||||
'if ! sudo mv -T -- "$registry_previous" "$registry_root"; then',
|
||||
'if ! sudo mv -T -- "$registry_root" "$registry_staging/failed-dwh-auth"; then',
|
||||
)
|
||||
restore_positions = [text["server"].find(step) for step in restore_steps]
|
||||
if any(position < 0 for position in restore_positions) or restore_positions != sorted(restore_positions):
|
||||
raise SystemExit("dwh-auth docs: restore must stage/check then use guarded same-filesystem renames")
|
||||
if text["server"].count('if ! sudo mv -T -- "$registry_previous" "$registry_root"; then') != 2:
|
||||
raise SystemExit("dwh-auth docs: every restore rollback rename must use guarded mv -T --")
|
||||
if re.search(r"sudo tar[^\n]*-C /var/lib[^\n]*(?:-x|--extract)[^\n]*registry_backup", text["server"]):
|
||||
raise SystemExit("dwh-auth docs: restore must not overlay the live registry")
|
||||
|
||||
|
||||
journal_steps = (
|
||||
"subprocess.run(",
|
||||
"[\"journalctl\", \"-u\", \"dwh-auth\", \"--since\", sys.argv[1]",
|
||||
"sys.argv[2:]",
|
||||
"stderr=subprocess.DEVNULL",
|
||||
"if result.returncode != 0:",
|
||||
"result.stdout.splitlines(keepends=True)",
|
||||
"any(key in line for key in actual_keys)",
|
||||
)
|
||||
if any(step not in text["server"] for step in journal_steps):
|
||||
raise SystemExit("dwh-auth docs: journal scan must fail closed and check the actual key bytes")
|
||||
|
||||
print("dwh-auth documentation contract passed")
|
||||
PY
|
||||
|
||||
Reference in New Issue
Block a user