Skip to content

Commit d6913c4

Browse files
feat: add redacted FALCON_DEBUG mode for bash and PowerShell (#522)
Support currently has to reach for `bash -x` or `Set-PSDebug -Trace` to diagnose a failing install, and both print credentials. This adds an opt-in debug mode that answers the questions support actually asks, without ever printing a value that is not known to be safe. bash gets `FALCON_DEBUG=1` or `--debug`; PowerShell gets `-FalconDebug` or `$env:FALCON_DEBUG`. Markers cover the detected OS, architecture, kernel and package manager; the exact FQL sensor query filter and how many installers matched; which installer was selected, its size and SHA-256 check; the API route, HTTP status and curl exit code for every call; the resolved sensor update policy version; and the installed sensor version and AID. The filter is the point of the whole feature. "No sensor found for OS" and "why did it pick that version" are unanswerable without seeing the query that was sent, and it is assembled from four separately derived pieces. Redaction is fail-closed. Only keys on a fixed allow-list keep their value; everything else becomes `[DROPPED]`, and a bare token with no key is dropped entirely. Anything user-supplied that could hold a secret is reported as presence only, under a distinct `_set` key — `provisioning_token` is not allow-listed, so a future line that prints it still drops. Customer data is reported as counts, not contents. In PowerShell, values that can contain a space are passed through `-Pairs` rather than a joined string. An FQL filter can hold a multi-word policy name, and any scheme that splits a joined string would either mangle it or, worse, glue a bare word onto an allow-listed value and print it. Debug adds no request of its own. `curl_command` already dumps headers and buffers the body, so the marker reports the status and exit code it has computed; the curl invocation is untouched. The exit code is reported inside `handle_curl_error`, which receives it on every failure path, so no call site captures `$?` and none of the `||` chains change. Neither language re-enables tracing. The `set +x` guard and `Set-PSDebug -Off` both stay, and no curl or Invoke-WebRequest gains a verbose flag. The PowerShell catch blocks now record a status code instead of serializing the whole exception object into the on-disk log. READMEs document the new mode with sample output from real runs, and stop recommending `bash -x` and `Set-PSDebug -Trace` for support. Co-authored-by: Carlos Matos <carlos.matos@crowdstrike.com>
1 parent 29b737f commit d6913c4

12 files changed

Lines changed: 1282 additions & 140 deletions

File tree

bash/containers/falcon-container-sensor-pull/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Please refer to the [Deprecation](DEPRECATION.md) document for more information
1515
- [Prerequisites](#prerequisites)
1616
- [Auto-Discovery of Falcon Cloud Region](#auto-discovery-of-falcon-cloud-region)
1717
- [Usage](#usage)
18+
- [Troubleshooting](#troubleshooting)
1819

1920
## Multi-Architecture Support :rocket:
2021

@@ -135,6 +136,10 @@ Optional Flags:
135136
--get-cid Get the CID assigned to the API Credentials
136137
--list-tags List all tags available for the selected sensor type and platform, sorted in ascending order
137138
--allow-legacy-curl Deprecated. Accepted and ignored; no longer needed
139+
--debug Print redacted progress markers to stderr (or set FALCON_DEBUG=1).
140+
Step name, HTTP status, cloud/region and curl exit only; values are
141+
dropped unless the key is on a fixed allow-list, so secrets cannot
142+
appear. Do not use bash -x for support; it prints credentials.
138143
139144
Internal Flags:
140145
--internal-build-stage <BUILD_STAGE> (Internal only) Falcon Build Stage [release|stage] (Default: release)
@@ -166,6 +171,7 @@ Help Options:
166171
| `--get-cid` | N/A | `None` | Get the CID assigned to the API Credentials. |
167172
| `--list-tags` | `$LISTTAGS` | `False` (Optional) | List all tags available for the selected sensor |
168173
| `--allow-legacy-curl` | `$ALLOW_LEGACY_CURL` | `False` (Optional) | Deprecated. Accepted and ignored; no longer needed |
174+
| `--debug` | `$FALCON_DEBUG` | `unset` (Optional) | Print redacted progress markers to stderr. Step, HTTP status, cloud/region and curl exit only; values are dropped unless the key is allow-listed, so secrets cannot appear. Do not use `bash -x` for support. |
169175
| `-h`, `--help` | N/A | `None` | Display help message |
170176

171177
---
@@ -438,3 +444,30 @@ The following example will pull the `falcon-sensor` image for the `x86_64` platf
438444
--type falcon-sensor \
439445
--platform x86_64
440446
```
447+
448+
---
449+
450+
## Troubleshooting
451+
452+
Use the redacted debug mode. It prints, to stderr: the detected OS, architecture,
453+
kernel and package manager; the exact sensor query filter and how many installers
454+
matched; which installer was selected, its size and SHA-256 check; the API route,
455+
HTTP status and curl exit code for every call; and the installed sensor version
456+
and AID. Values are dropped unless the key is on a fixed allow-list, so
457+
credentials cannot appear in the output you send to support.
458+
459+
```shell
460+
./falcon-container-sensor-pull.sh \
461+
--client-id <FALCON_CLIENT_ID> \
462+
--client-secret <FALCON_CLIENT_SECRET> \
463+
--type falcon-sensor \
464+
--debug
465+
```
466+
467+
`FALCON_DEBUG=1` does the same thing, which is useful when the script runs from a
468+
pipe or a job where you cannot add a flag.
469+
470+
Do **not** use `bash -x` for support. It prints every expanded command, including
471+
`client_secret`, access tokens, the registry password and `Authorization`
472+
headers. This script turns tracing off at startup and warns when it does, but a
473+
trace enabled before that point can still expose credentials.

bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,54 @@ unset FALCON_CLIENT_SECRET
1212
FALCON_CLIENT_SECRET=$falcon_client_secret
1313
unset falcon_client_secret
1414

15+
# Opt-in redacted debug. Never re-enable set -x around credential paths.
16+
falcon_debug_enabled() {
17+
case "${FALCON_DEBUG:-}" in
18+
1 | true) return 0 ;;
19+
*) return 1 ;;
20+
esac
21+
}
22+
23+
# Allow-list. Only known-safe keys keep their value; everything else is dropped,
24+
# so a future debug line cannot leak a secret by accident.
25+
falcon_debug_filter() {
26+
printf '%s\n' "$@" | awk '
27+
BEGIN {
28+
split("step source error stage \
29+
cloud old_cloud new_cloud region region_hint sensor_cloud \
30+
http_status curl_exit exit_code path filter sort \
31+
os os_version os_arch os_family kernel pkg_manager distro_id run_as \
32+
count index decrement version sensor_version policy_version file_type sha \
33+
installer bytes sha_verify billing backend apd aid cid_source \
34+
tags_count grouping_tags_count sensor_type param registry repository tag \
35+
client_id_set client_secret_set access_token_set member_cid_set \
36+
provisioning_token_set maintenance_token_set proxy_set policy_name_set \
37+
tags_set grouping_tags_set", safe, " ")
38+
for (i in safe) { ok[safe[i]] = 1 }
39+
}
40+
{
41+
eq = index($0, "=")
42+
if (eq < 2) { next }
43+
key = substr($0, 1, eq - 1)
44+
printf " %s=%s", key, (key in ok) ? substr($0, eq + 1) : "[DROPPED]"
45+
}
46+
'
47+
}
48+
49+
falcon_debug() {
50+
falcon_debug_enabled || return 0
51+
local falcon_debug_label
52+
falcon_debug_label=$1
53+
shift
54+
printf 'FALCON_DEBUG: %s%s\n' "$falcon_debug_label" "$(falcon_debug_filter "$@")" >&2
55+
}
56+
57+
# Last HTTP status from a curl --dump-header file. Status only — no header dump.
58+
falcon_debug_http_status() {
59+
[ -f "$1" ] || return 0
60+
grep -i '^HTTP/' "$1" 2>/dev/null | tail -n 1 | awk '{print $2}'
61+
}
62+
1563
: <<'#DESCRIPTION#'
1664
File: falcon-container-sensor-pull.sh
1765
Description: Bash script to copy Falcon DaemonSet Sensor, Container Sensor, or Kubernetes Admission Controller images from CrowdStrike Container Registry.
@@ -72,6 +120,11 @@ Optional Flags:
72120
--get-cid Get the CID assigned to the API Credentials
73121
--list-tags List all tags available for the selected sensor type and platform, sorted in ascending order
74122
--allow-legacy-curl Deprecated. Accepted and ignored; no longer needed
123+
--debug Print redacted progress markers to stderr (or set FALCON_DEBUG=1).
124+
Sensor type, resolved registry/repository/tag, how many tags matched,
125+
the API route, HTTP status and curl exit code. Values are dropped
126+
unless the key is on a fixed allow-list, so secrets cannot appear.
127+
Do not use bash -x for support; it prints credentials.
75128
76129
Internal Flags:
77130
--internal-build-stage <BUILD_STAGE> (Internal only) Falcon Build Stage [release|stage] (Default: release)
@@ -202,6 +255,9 @@ while [ $# != 0 ]; do
202255
ALLOW_LEGACY_CURL=true
203256
fi
204257
;;
258+
--debug)
259+
FALCON_DEBUG=1
260+
;;
205261
-n | --node)
206262
if [ -n "${1}" ]; then
207263
deprecated "-n|--node"
@@ -243,6 +299,10 @@ while [ $# != 0 ]; do
243299
shift
244300
done
245301

302+
falcon_debug start "version=$VERSION" "cloud=${FALCON_CLOUD:-unset}" "sensor_type=${SENSOR_TYPE:-unset}" \
303+
"client_id_set=$([ -n "${FALCON_CLIENT_ID}" ] && echo yes || echo no)" \
304+
"client_secret_set=$([ -n "${FALCON_CLIENT_SECRET}" ] && echo yes || echo no)"
305+
246306
if ! command -v curl >/dev/null 2>&1; then
247307
die "The 'curl' command is missing. Please install it before continuing. Aborting..."
248308
fi
@@ -255,6 +315,8 @@ fi
255315
handle_curl_error() {
256316
local err_msg
257317

318+
falcon_debug handle_curl_error "curl_exit=$1"
319+
258320
if [ "$1" = "28" ]; then
259321
err_msg="Operation timed out (exit code 28). If using a proxy, please check your proxy settings."
260322
die "$err_msg"
@@ -284,14 +346,31 @@ handle_curl_error() {
284346

285347
curl_command() {
286348
# Dash does not support arrays, so we have to pass the args as separate arguments
287-
local token="$1" escaped_token auth_config headers body status hint old_host new_host arg rc
349+
local token="$1" escaped_token auth_config headers body status hint old_host new_host arg rc req_path
288350
shift
289351
# The configuration value must be quoted, because it holds a space and a
290352
# colon. curl processes backslash escapes inside a quoted value, so a
291353
# backslash or a double quote in the token has to be escaped first.
292354
escaped_token=$(printf '%s' "$token" | sed 's/\\/\\\\/g; s/"/\\"/g')
293355
auth_config=$(printf 'header = "Authorization: Bearer %s"' "$escaped_token")
294356

357+
# API route only, for the debug marker. The query string is dropped: it can
358+
# carry an installer id, and the route alone identifies the call.
359+
req_path=""
360+
for arg in "$@"; do
361+
case "$arg" in
362+
https://*)
363+
req_path=${arg#https://}
364+
case "$req_path" in
365+
*/*) req_path=/${req_path#*/} ;;
366+
*) req_path=/ ;;
367+
esac
368+
req_path=${req_path%%\?*}
369+
break
370+
;;
371+
esac
372+
done
373+
295374
headers=$(mktemp)
296375
body=$(mktemp)
297376
# No -L: the bearer token must never cross a redirect hop. The body is held
@@ -304,6 +383,7 @@ curl_command() {
304383
# Re-issue against that region instead of following Location. The registry is
305384
# a different host, so its URLs are never rewritten.
306385
status=$(awk '/^HTTP\//{s=$2} END{print s}' "$headers")
386+
falcon_debug curl_command "path=$req_path" "http_status=$status" "curl_exit=$rc"
307387
case "$status" in
308388
301 | 302 | 307 | 308)
309389
hint=$(grep -i ^x-cs-region: "$headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
@@ -325,6 +405,7 @@ curl_command() {
325405
printf '%s\n' "$auth_config" |
326406
curl -s --proto '=https' -K- "$@" >"$body"
327407
rc=$?
408+
falcon_debug curl_command "step=region_retry" "path=$req_path" "region=$hint" "curl_exit=$rc"
328409
fi
329410
fi
330411
;;
@@ -336,6 +417,7 @@ curl_command() {
336417
}
337418

338419
fetch_tags() {
420+
falcon_debug fetch_tags "step=registry_token"
339421
bearer_result=$(echo "-u $ART_USERNAME:$ART_PASSWORD" |
340422
curl -s --proto '=https' \
341423
"https://$cs_registry/v2/token?account=$ART_USERNAME&scope=repository:$registry_opts/$repository_name:pull&service=$cs_registry" -K-) || handle_curl_error $?
@@ -430,13 +512,26 @@ is_multi_arch() {
430512
fi
431513
}
432514

515+
# Runs a container tool command with set -e off just long enough to capture its
516+
# real exit code for the debug marker, then returns that code unchanged.
517+
run_container_cmd() {
518+
local step="$1" rc
519+
shift
520+
set +e
521+
"$@"
522+
rc=$?
523+
set -e
524+
falcon_debug "$step" "exit_code=$rc"
525+
return "$rc"
526+
}
527+
433528
pull_image() {
434529
local image_path="$1"
435530
local platform_override="$2"
436531
if [ -n "$platform_override" ]; then
437-
"$CONTAINER_TOOL" pull --platform "$platform_override" "$image_path"
532+
run_container_cmd pull_image "$CONTAINER_TOOL" pull --platform "$platform_override" "$image_path"
438533
else
439-
"$CONTAINER_TOOL" pull "$image_path"
534+
run_container_cmd pull_image "$CONTAINER_TOOL" pull "$image_path"
440535
fi
441536
}
442537

@@ -447,18 +542,18 @@ copy_image() {
447542
if [ "$multi_arch_copy" = "true" ]; then
448543
case "${CONTAINER_TOOL}" in
449544
*skopeo)
450-
"$CONTAINER_TOOL" copy --all "docker://$source_path" "docker://$destination_path"
545+
run_container_cmd copy_image "$CONTAINER_TOOL" copy --all "docker://$source_path" "docker://$destination_path"
451546
;;
452547
*podman)
453-
"$CONTAINER_TOOL" manifest create --all "$destination_path" "$source_path" >/dev/null
454-
"$CONTAINER_TOOL" manifest push --all "$destination_path"
455-
"$CONTAINER_TOOL" manifest rm "$destination_path" >/dev/null
548+
run_container_cmd copy_image "$CONTAINER_TOOL" manifest create --all "$destination_path" "$source_path" >/dev/null &&
549+
run_container_cmd copy_image "$CONTAINER_TOOL" manifest push --all "$destination_path" &&
550+
"$CONTAINER_TOOL" manifest rm "$destination_path" >/dev/null
456551
;;
457552
*docker)
458553
if ! "$CONTAINER_TOOL" buildx version >/dev/null 2>&1; then
459554
die "Docker buildx is not installed/enabled. Please install/enable buildx before continuing."
460555
else
461-
"$CONTAINER_TOOL" buildx imagetools create --tag "$destination_path" "$source_path"
556+
run_container_cmd copy_image "$CONTAINER_TOOL" buildx imagetools create --tag "$destination_path" "$source_path"
462557
fi
463558
;;
464559
*)
@@ -467,8 +562,8 @@ copy_image() {
467562
esac
468563
else
469564
# Copy the image to the desired registry
470-
"$CONTAINER_TOOL" tag "$source_path" "$destination_path"
471-
"$CONTAINER_TOOL" push "$destination_path"
565+
run_container_cmd copy_image "$CONTAINER_TOOL" tag "$source_path" "$destination_path" &&
566+
run_container_cmd copy_image "$CONTAINER_TOOL" push "$destination_path"
472567
fi
473568
}
474569

@@ -594,14 +689,17 @@ match_sensor_version() {
594689
local all_tags
595690
local matched_tags
596691
local version_pattern
692+
local chosen
597693

598694
# Get all available tags by properly parsing JSON output from list_tags
599695
all_tags=$(extract_raw_tags)
600696

601697
if [ -z "$requested_version" ]; then
602698
# If no version specified, get the latest version
603699
if [ -n "$all_tags" ]; then
604-
echo "$all_tags" | sort -V | tail -1
700+
chosen=$(echo "$all_tags" | sort -V | tail -1)
701+
falcon_debug match_sensor_version "count=$(echo "$all_tags" | grep -c .)" "tag=$chosen"
702+
echo "$chosen"
605703
return 0
606704
else
607705
return 1
@@ -613,7 +711,9 @@ match_sensor_version() {
613711
matched_tags=$(echo "$all_tags" | grep -E "$version_pattern")
614712

615713
if [ -n "$matched_tags" ]; then
616-
echo "$matched_tags" | sort -V | tail -1
714+
chosen=$(echo "$matched_tags" | sort -V | tail -1)
715+
falcon_debug match_sensor_version "count=$(echo "$matched_tags" | grep -c .)" "tag=$chosen"
716+
echo "$chosen"
617717
return 0
618718
fi
619719

@@ -635,7 +735,9 @@ match_sensor_version() {
635735
fi
636736

637737
if [ -n "$matched_tags" ]; then
638-
echo "$matched_tags" | sort -V | tail -1
738+
chosen=$(echo "$matched_tags" | sort -V | tail -1)
739+
falcon_debug match_sensor_version "count=$(echo "$matched_tags" | grep -c .)" "tag=$chosen"
740+
echo "$chosen"
639741
return 0
640742
fi
641743

@@ -735,7 +837,9 @@ cs_falcon_oauth_token=$(
735837
736838
auth_payload="client_id=$FALCON_CLIENT_ID&client_secret=$FALCON_CLIENT_SECRET"
737839
840+
falcon_debug oauth2_token "step=request" "cloud=${FALCON_CLOUD:-unset}"
738841
token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "$response_headers") || handle_curl_error $?
842+
falcon_debug oauth2_token "step=response" "http_status=$(falcon_debug_http_status "$response_headers")" "cloud=${FALCON_CLOUD:-unset}"
739843
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
740844
if [ -z "$token" ]; then
741845
# Wrong region: retry against the x-cs-region hint instead of following
@@ -749,7 +853,9 @@ cs_falcon_oauth_token=$(
749853
# Separate file: --dump-header truncates, and region_hint below
750854
# still needs the original response.
751855
retry_headers=$(mktemp)
856+
falcon_debug oauth2_token "step=retry" "region=$hinted"
752857
token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers") || handle_curl_error $?
858+
falcon_debug oauth2_token "step=retry_response" "http_status=$(falcon_debug_http_status "$retry_headers")" "region=$hinted"
753859
rm -f "$retry_headers"
754860
token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g')
755861
fi
@@ -762,6 +868,7 @@ cs_falcon_oauth_token=$(
762868
)
763869

764870
region_hint=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g')
871+
falcon_debug oauth2_token "region_hint=${region_hint:-none}" "cloud=${FALCON_CLOUD:-unset}"
765872
rm "${response_headers}"
766873

767874
if [ "${FALCON_CLOUD}" != "${region_hint}" ] && [ -n "${region_hint}" ]; then
@@ -1030,6 +1137,8 @@ fi
10301137
#Construct full image path
10311138
FULLIMAGEPATH="${REPOSITORY}:${LATESTSENSOR}"
10321139

1140+
falcon_debug main "registry=$cs_registry" "repository=$REPOSITORY" "tag=$LATESTSENSOR"
1141+
10331142
if [ "$GETIMAGEPATH" ]; then
10341143
echo "${FULLIMAGEPATH}"
10351144
exit 0
@@ -1065,7 +1174,7 @@ if [ "$(is_multi_arch "$FULLIMAGEPATH")" = "true" ]; then
10651174
if [ -n "$SENSOR_PLATFORM" ]; then
10661175
# If Skopeo is being used, the platform must be overridden
10671176
if grep -qw "skopeo" "$CONTAINER_TOOL"; then
1068-
"$CONTAINER_TOOL" copy --override-arch "$(platform_override)" --override-os linux "docker://$FULLIMAGEPATH" "docker://$COPYPATH"
1177+
run_container_cmd skopeo_copy "$CONTAINER_TOOL" copy --override-arch "$(platform_override)" --override-os linux "docker://$FULLIMAGEPATH" "docker://$COPYPATH"
10691178
else
10701179
# Podman/Docker can pull the specific platform
10711180
pf_override="linux/$(platform_override)"
@@ -1094,7 +1203,7 @@ You can either:
10941203
else
10951204
# Handle non-multi-arch images
10961205
if grep -qw "skopeo" "$CONTAINER_TOOL"; then
1097-
"$CONTAINER_TOOL" copy "docker://$FULLIMAGEPATH" "docker://$COPYPATH"
1206+
run_container_cmd skopeo_copy "$CONTAINER_TOOL" copy "docker://$FULLIMAGEPATH" "docker://$COPYPATH"
10981207
else
10991208
pull_image "$FULLIMAGEPATH"
11001209

0 commit comments

Comments
 (0)