From d8dcb84edb1671872d20ff4ea5f03ddba75fe01c Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Wed, 26 Aug 2026 01:06:48 -0500 Subject: [PATCH] harden production debug wrapper --- docs/install_debug_charts.md | 4 ++++ docs/plan_diagnostics_improvements.md | 5 +++++ ops/chart-debug-command | 20 +++++++++++++++----- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/install_debug_charts.md b/docs/install_debug_charts.md index 85a160c..638ee37 100644 --- a/docs/install_debug_charts.md +++ b/docs/install_debug_charts.md @@ -45,6 +45,10 @@ on the diagnostics client at: - Preserve the `restrict` and forced-command options in `authorized_keys`. - Verify arbitrary commands and malformed arguments are denied. - Do not print container environment variables or application secrets. +- Ensure `status` passes its token to curl through stdin, not an argv `-H` + argument visible through `/proc`. +- Resolve the container into a variable before Docker calls so selector failure + propagates with the documented return code. The account may run only: diff --git a/docs/plan_diagnostics_improvements.md b/docs/plan_diagnostics_improvements.md index f1d7c13..2b439f5 100644 --- a/docs/plan_diagnostics_improvements.md +++ b/docs/plan_diagnostics_improvements.md @@ -109,6 +109,11 @@ Security invariants: fail closed. - Verify denials as well as successful commands. Audit records are available via `journalctl -t chart-debug`. +- Keep secrets off process command lines. The status command supplies + `CHART_AUTH_TOKEN` to curl through stdin (`curl --config -`), never `-H`. +- Assign `resolve_container` before invoking Docker. Calling it inline through + command substitution can trap its exit in a subshell and obscure the intended + fail-closed return code. ## Observability diff --git a/ops/chart-debug-command b/ops/chart-debug-command index 09b963f..d6d4fd6 100755 --- a/ops/chart-debug-command +++ b/ops/chart-debug-command @@ -53,7 +53,8 @@ case "${args[0]}" in [[ "${args[2]}" =~ ^[1-9][0-9]*(s|m|h|d)$ ]] || { echo "invalid duration" >&2; exit 2; } - docker logs --timestamps --tail "$MAX_LOG_LINES" --since "${args[2]}" "$(resolve_container)" 2>&1 | redact + container=$(resolve_container) + docker logs --timestamps --tail "$MAX_LOG_LINES" --since "${args[2]}" "$container" 2>&1 | redact ;; status) [[ ${#args[@]} -eq 1 ]] || { echo "usage: status" >&2; exit 2; } @@ -63,7 +64,13 @@ case "${args[0]}" in [[ "$entry" == CHART_AUTH_TOKEN=* ]] && token=${entry#CHART_AUTH_TOKEN=} done < <(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$container") if [[ -n "$token" ]]; then - curl -fsS -H "X-Chart-Token: $token" "$CHART_PUBLIC_URL/api/status" | redact + [[ "$token" != *$'\n'* && "$token" != *$'\r'* ]] || { + echo "invalid CHART_AUTH_TOKEN" >&2; exit 4; + } + escaped_token=${token//\\/\\\\} + escaped_token=${escaped_token//\"/\\\"} + printf 'url = "%s/api/status"\nheader = "X-Chart-Token: %s"\nfail\nsilent\nshow-error\n' \ + "$CHART_PUBLIC_URL" "$escaped_token" | curl --config - | redact else echo "CHART_AUTH_TOKEN is unavailable" >&2 exit 4 @@ -71,21 +78,24 @@ case "${args[0]}" in ;; container-state) [[ ${#args[@]} -eq 1 ]] || { echo "usage: container-state" >&2; exit 2; } + container=$(resolve_container) docker inspect --format \ 'name={{.Name}} image={{.Config.Image}} status={{.State.Status}} started={{.State.StartedAt}} restarts={{.RestartCount}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}}' \ - "$(resolve_container)" + "$container" ;; recent-deploy) [[ ${#args[@]} -eq 1 ]] || { echo "usage: recent-deploy" >&2; exit 2; } curl -fsS "$CHART_PUBLIC_URL/api/version" printf '\n' - docker inspect --format 'container_started={{.State.StartedAt}} image={{.Config.Image}}' "$(resolve_container)" + container=$(resolve_container) + docker inspect --format 'container_started={{.State.StartedAt}} image={{.Config.Image}}' "$container" ;; capture-read) [[ ${#args[@]} -eq 2 ]] && valid_capture_id "${args[1]}" || { echo "usage: capture-read c-XXXXXXXXXXXX" >&2; exit 2; } - docker exec "$(resolve_container)" base64 "/tmp/chart-captures/${args[1]}.png" + container=$(resolve_container) + docker exec "$container" base64 "/tmp/chart-captures/${args[1]}.png" ;; capture-delete) [[ ${#args[@]} -eq 2 ]] && valid_capture_id "${args[1]}" || {