diff --git a/.github/scripts/install-test-deps.sh b/.github/scripts/install-test-deps.sh new file mode 100644 index 0000000..3016bfa --- /dev/null +++ b/.github/scripts/install-test-deps.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Installs the packages that .github/scripts/test-curl-oauth-live.sh needs: +# curl and a python interpreter. Only missing packages are installed, so the +# script does nothing on images that already have both. RHEL/CentOS 7 images +# supply curl and python2 already, so no package manager runs there. + +set -eu + +have() { + command -v "$1" >/dev/null 2>&1 +} + +have_python() { + have python3 || have python2 || have python +} + +if have curl && have_python; then + echo 'curl and python are already present; no packages to install.' + exit 0 +fi + +if have apk; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + # shellcheck disable=SC2086 # deliberate word splitting into package names + apk add --no-cache $packages +elif have dnf; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + # shellcheck disable=SC2086 # deliberate word splitting into package names + dnf install -y -q $packages +elif have yum; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python" + # shellcheck disable=SC2086 # deliberate word splitting into package names + yum install -y -q $packages +elif have apt-get; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + apt-get update -qq + # shellcheck disable=SC2086 # deliberate word splitting into package names + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $packages +else + echo 'ERROR: no supported package manager found (apk, dnf, yum, apt-get).' >&2 + exit 1 +fi + +echo 'Test dependencies are installed.' diff --git a/.github/scripts/test-credential-handling.sh b/.github/scripts/test-credential-handling.sh new file mode 100644 index 0000000..9a3db03 --- /dev/null +++ b/.github/scripts/test-credential-handling.sh @@ -0,0 +1,167 @@ +#!/bin/bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# Report an absent or broken search tool as a failure instead of a pass. grep +# returns 0 for a match, 1 for no match, and 2 or more for an error. Only 1 is +# an acceptable result here. +assert_no_match() { + local description=$1 pattern=$2 tree=$3 include=$4 + local output status + + set +e + output=$(grep -rnE "$pattern" "$repo_root/$tree" --include="$include") + status=$? + set -e + + if [ "$status" -eq 0 ]; then + echo "$output" >&2 + fail "$description" + fi + if [ "$status" -ne 1 ]; then + fail "grep failed with status $status while checking: $description" + fi +} + +curl() { + printf '%s\n' "$@" >"$CURL_ARGS_FILE" + cat >"$CURL_STDIN_FILE" +} + +test_curl_helper() { + local script=$1 token_mode=$2 + local helper mode expected_key + + helper=$(awk '/^curl_command\(\)/,/^}/' "$repo_root/$script") + [ -n "$helper" ] || fail "curl_command not found in $script" + eval "$helper" + + # Mode 1 uses the oauth2-bearer configuration key. Mode 0 is the fallback + # for curl older than 7.33.0 and uses a raw Authorization header. Both must + # keep the credential on stdin. + for mode in 1 0; do + # shellcheck disable=SC2034 # read by the curl_command body under eval + curl_has_oauth2_bearer=$mode + if [ "$mode" -eq 1 ]; then + expected_key='oauth2-bearer = "REGRESSION_SECRET_TOKEN"' + else + expected_key='header = "Authorization: Bearer REGRESSION_SECRET_TOKEN"' + fi + + CURL_ARGS_FILE="$work_dir/args" + CURL_STDIN_FILE="$work_dir/stdin" + # shellcheck disable=SC2034 # read by the curl_command body under eval + proxy="" + cs_falcon_oauth_token="REGRESSION_SECRET_TOKEN" + + if [ "$token_mode" = "argument" ]; then + curl_command "$cs_falcon_oauth_token" "https://api.example.invalid/resource" + else + curl_command "https://api.example.invalid/resource" + fi + + if grep -qF "$cs_falcon_oauth_token" "$CURL_ARGS_FILE"; then + fail "$script exposed the bearer token in curl arguments (mode=$mode)" + fi + grep -qF 'https://api.example.invalid/resource' "$CURL_ARGS_FILE" || + fail "$script did not pass the expected URL (mode=$mode)" + grep -qF "$cs_falcon_oauth_token" "$CURL_STDIN_FILE" || + fail "$script did not provide the bearer token through stdin (mode=$mode)" + grep -qF "$expected_key" "$CURL_STDIN_FILE" || + fail "$script did not use the expected credential mechanism (mode=$mode)" + grep -qF -- '--proto' "$CURL_ARGS_FILE" || + fail "$script did not restrict the request protocol (mode=$mode)" + grep -qF -- '--proto-redir' "$CURL_ARGS_FILE" || + fail "$script did not restrict the redirect protocol (mode=$mode)" + done +} + +test_curl_helper \ + bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh argument +test_curl_helper bash/install/falcon-linux-install.sh global +test_curl_helper bash/install/falcon-linux-uninstall.sh global +test_curl_helper bash/migrate/falcon-linux-migrate.sh global + +test_xtrace_guard() { + local script=$1 guard trace_file + + guard=$(awk '/^case \$- in$/,/^esac$/' "$repo_root/$script") + [ -n "$guard" ] || fail "xtrace guard not found in $script" + trace_file="$work_dir/xtrace" + + FALCON_CLIENT_SECRET="XTRACE_SECRET_SENTINEL" \ + bash -xc "$guard; : \"\$FALCON_CLIENT_SECRET\"" \ + >/dev/null 2>"$trace_file" + + if grep -qF 'XTRACE_SECRET_SENTINEL' "$trace_file"; then + fail "$script allowed a credential into bash xtrace output" + fi +} + +test_xtrace_guard bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh +test_xtrace_guard bash/install/falcon-linux-install.sh +test_xtrace_guard bash/install/falcon-linux-uninstall.sh +test_xtrace_guard bash/migrate/falcon-linux-migrate.sh + +test_hash_verification() { + local script=$1 helper test_file expected_sha + + helper=$(awk '/^verify_sha256\(\)/,/^}/' "$repo_root/$script") + [ -n "$helper" ] || fail "verify_sha256 not found in $script" + ( + eval "$helper" + # verify_sha256 calls die on a mismatch; keep the stub inside the subshell. + # shellcheck disable=SC2329 # invoked indirectly by verify_sha256 + die() { exit 1; } + + test_file="$work_dir/installer" + printf '%s' 'verified installer content' >"$test_file" + expected_sha=$(openssl dgst -sha256 "$test_file" | awk '{ print $NF }') + verify_sha256 "$test_file" "$expected_sha" || + fail "$script rejected a valid installer hash" + + if (verify_sha256 "$test_file" '0000000000000000000000000000000000000000000000000000000000000000'); then + fail "$script accepted an invalid installer hash" + fi + [ ! -e "$test_file" ] || fail "$script retained an installer with an invalid hash" + ) +} + +test_hash_verification bash/install/falcon-linux-install.sh +test_hash_verification bash/migrate/falcon-linux-migrate.sh + +# The arguments below are grep patterns, not shell expansions. +# shellcheck disable=SC2016 +assert_no_match \ + 'a Bash error path exposes a credential or raw maintenance-token response' \ + 'Invalid Access Token:.*\$cs_falcon_oauth_token|Failed to retrieve maintenance token\. Response:' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'an EC2 metadata token is exposed in curl arguments' \ + 'curl .*X-aws-ec2-metadata-token:.*\$token' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'an AWS SSM Parameter Store error path prints the decrypted response body' \ + 'AWS SSM Parameter Store[^"]*\$response' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'a PowerShell log statement exposes an authentication or installer token' \ + '(Invoke-FalconAuth|GetToken) - \$content:|Retrieved maintenance token:|Starting .*parameters.*\$(Install|Uninstall)Params' \ + powershell '*.ps1' + +echo 'PASS: credential handling regression checks' diff --git a/.github/scripts/test-curl-oauth-live.sh b/.github/scripts/test-curl-oauth-live.sh new file mode 100644 index 0000000..fba80f2 --- /dev/null +++ b/.github/scripts/test-curl-oauth-live.sh @@ -0,0 +1,177 @@ +#!/bin/sh + +# Confirms how the installed curl handles an OAuth 2 bearer credential that +# arrives on its configuration input. The script adapts to the curl it finds, +# so it runs on RHEL/CentOS 7 (curl 7.29.0, python2) with no extra packages. + +set -eu + +work_dir=$(mktemp -d) +server_pids="" + +cleanup() { + for pid in $server_pids; do + kill "$pid" 2>/dev/null || true + done + rm -rf "$work_dir" +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +python_bin="" +for candidate in python3 python2 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python_bin=$candidate + break + fi +done +[ -n "$python_bin" ] || fail 'no python interpreter is available for the capture server' + +server_script="$work_dir/capture_server.py" +cat >"$server_script" <<'PY' +import sys + +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: # Python 2, which is all that RHEL/CentOS 7 provides. + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +output_path = sys.argv[1] +port = int(sys.argv[2]) +redirect_to = sys.argv[3] if len(sys.argv) > 3 else "" + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + # The marker shows that a request arrived. Without it, an empty capture + # file cannot show the difference between a credential that curl + # removed and a connection that never happened. + with open(output_path, "w") as handle: + handle.write("REQUEST_ARRIVED|auth=[%s]" + % self.headers.get("Authorization", "")) + if redirect_to: + self.send_response(302) + self.send_header("Location", redirect_to) + else: + self.send_response(204) + self.end_headers() + + def log_message(self, *args): + pass + + +HTTPServer.allow_reuse_address = True +# Bind every interface. curl can resolve "localhost" to ::1, and a server bound +# only to 127.0.0.1 refuses that connection and gives a false failure. +server = HTTPServer(("0.0.0.0", port), Handler) + +with open(output_path + ".ready", "w") as handle: + handle.write("ready") + +# Serve one request, then stop. The timeout makes sure the process always ends. +server.timeout = 20 +server.handle_request() +PY + +start_server() { + output=$1 + port=$2 + redirect_to=${3:-} + + rm -f "$output" "$output.ready" + "$python_bin" "$server_script" "$output" "$port" "$redirect_to" & + server_pids="$server_pids $!" +} + +wait_for_server() { + output=$1 + attempt=0 + + while [ "$attempt" -lt 15 ]; do + if [ -f "$output.ready" ]; then + return 0 + fi + sleep 1 + attempt=$((attempt + 1)) + done + fail "the capture server for $output did not start" +} + +assert_marker() { + file=$1 + expected=$2 + description=$3 + actual="" + + [ -s "$file" ] || fail "$description (no request reached the capture server)" + actual=$(cat "$file") + [ "$actual" = "$expected" ] || + fail "$description (expected '$expected', received '$actual')" +} + +auth_config_for() { + case $1 in + oauth2-bearer) printf 'oauth2-bearer = "%s"\n' "$token" ;; + header) printf 'header = "Authorization: Bearer %s"\n' "$token" ;; + *) fail "unknown credential mechanism: $1" ;; + esac +} + +token=CONTAINER_LIVE_TEST_TOKEN +curl --version | head -n 1 + +# curl 7.33.0 added the oauth2-bearer option. Compare the version with awk +# because busybox sort has no -V option. +if curl --version | head -n 1 | + awk '{ split($2, v, "."); exit !(v[1] > 7 || (v[1] == 7 && v[2] >= 33)) }'; then + active_mode=oauth2-bearer +else + active_mode=header +fi +echo "Credential mechanism under test: $active_mode" + +# 1. The mechanism this curl uses must deliver the credential to the same host. +start_server "$work_dir/direct" 28768 +wait_for_server "$work_dir/direct" +auth_config_for "$active_mode" | + curl --silent --show-error -K- --url http://127.0.0.1:28768/test +assert_marker "$work_dir/direct" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + "the $active_mode mechanism did not transmit the credential" + +# 2. curl must remove the credential when a redirect crosses to another host. +start_server "$work_dir/redirect" 28769 'http://localhost:28770/target' +start_server "$work_dir/target" 28770 +wait_for_server "$work_dir/redirect" +wait_for_server "$work_dir/target" +auth_config_for "$active_mode" | + curl --silent --show-error -L -K- --url http://127.0.0.1:28769/start +assert_marker "$work_dir/redirect" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + 'the first request of the redirect chain did not carry the credential' +assert_marker "$work_dir/target" 'REQUEST_ARRIVED|auth=[]' \ + "the $active_mode credential crossed a redirect to another host" + +# 3. On a modern curl, also confirm that the older fallback still works. This +# keeps the fallback path tested on the machines that do not need it. +if [ "$active_mode" = "oauth2-bearer" ]; then + start_server "$work_dir/fallback" 28771 + wait_for_server "$work_dir/fallback" + auth_config_for header | + curl --silent --show-error -K- --url http://127.0.0.1:28771/test + assert_marker "$work_dir/fallback" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + 'the raw-header fallback did not transmit the credential' +fi + +# 4. A stray bare argument must not become a request. This is the guard that +# stops a leaked credential from reaching a name server. +if auth_config_for "$active_mode" | + curl --silent --show-error --proto '=https' --proto-redir '=https' -K- \ + "$token" >/dev/null 2>"$work_dir/proto-error"; then + fail 'the protocol guard accepted a bare argument as a request' +fi +echo "Protocol guard rejected a bare argument: $(cat "$work_dir/proto-error")" + +echo "PASS: curl credential handling ($active_mode mode)" diff --git a/.github/scripts/test-powershell-credential-handling.ps1 b/.github/scripts/test-powershell-credential-handling.ps1 new file mode 100644 index 0000000..05899aa --- /dev/null +++ b/.github/scripts/test-powershell-credential-handling.ps1 @@ -0,0 +1,65 @@ +$ErrorActionPreference = 'Stop' + +$RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '../..') +$Scripts = @( + 'powershell/install/falcon_windows_install.ps1' + 'powershell/install/falcon_windows_uninstall.ps1' + 'powershell/migrate/falcon_windows_migrate.ps1' +) +$SensitiveLogVariables = '(FalconAccessToken|FalconClientSecret|MaintenanceToken|ProvToken|InstallParams|UninstallParams)' +$Failures = [System.Collections.Generic.List[string]]::new() + +foreach ($RelativePath in $Scripts) { + $Path = Join-Path $RepositoryRoot $RelativePath + $Tokens = $null + $ParseErrors = $null + $Ast = [System.Management.Automation.Language.Parser]::ParseFile( + $Path, + [ref] $Tokens, + [ref] $ParseErrors + ) + + foreach ($ParseError in $ParseErrors) { + $Failures.Add("${RelativePath}:$($ParseError.Extent.StartLineNumber): parser error: $($ParseError.Message)") + } + + $LogCommands = $Ast.FindAll({ + param($Node) + $Node -is [System.Management.Automation.Language.CommandAst] -and + $Node.GetCommandName() -in @('Write-FalconLog', 'Write-VerboseLog') + }, $true) + + foreach ($Command in $LogCommands) { + $CommandText = $Command.Extent.Text + if ($CommandText -match "\`$$SensitiveLogVariables") { + $Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive variable used in log command") + } + if ($CommandText -match 'Write-VerboseLog' -and + $CommandText -match '\$content' -and + $CommandText -match '(Invoke-FalconAuth|GetToken)') { + $Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive API response used in verbose log") + } + } + + $DebugOffCommands = $Ast.FindAll({ + param($Node) + $Node -is [System.Management.Automation.Language.CommandAst] -and + $Node.GetCommandName() -eq 'Set-PSDebug' -and + $Node.Extent.Text -match '-Off' + }, $true) + if ($DebugOffCommands.Count -eq 0) { + $Failures.Add("${RelativePath}: PowerShell tracing is not disabled before credentials are processed") + } +} + +if ($Failures.Count -gt 0) { + # -ErrorAction Continue overrides $ErrorActionPreference = 'Stop' for these + # calls. Without it the first Write-Error throws, and only one of the + # collected failures is ever reported. + foreach ($Failure in $Failures) { + Write-Error $Failure -ErrorAction Continue + } + exit 1 +} + +Write-Output 'PASS: PowerShell credential logging and parser checks' diff --git a/.github/workflows/container_sensor_pull.yml b/.github/workflows/container_sensor_pull.yml index 7ec5b36..4fad84e 100644 --- a/.github/workflows/container_sensor_pull.yml +++ b/.github/workflows/container_sensor_pull.yml @@ -31,6 +31,9 @@ jobs: shellcheck --version shellcheck bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh + - name: Unit tests + run: sh bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh + container-test: name: Container Test needs: validate diff --git a/.github/workflows/credential_handling.yml b/.github/workflows/credential_handling.yml new file mode 100644 index 0000000..911e65b --- /dev/null +++ b/.github/workflows/credential_handling.yml @@ -0,0 +1,90 @@ +name: "CI: credential handling" + +on: + push: + paths: + - '.github/scripts/install-test-deps.sh' + - '.github/scripts/test-credential-handling.sh' + - '.github/scripts/test-curl-oauth-live.sh' + - '.github/scripts/test-powershell-credential-handling.ps1' + - '.github/workflows/credential_handling.yml' + - 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh' + - 'bash/install/falcon-linux-install.sh' + - 'bash/install/falcon-linux-uninstall.sh' + - 'bash/migrate/falcon-linux-migrate.sh' + - 'powershell/install/falcon_windows_install.ps1' + - 'powershell/install/falcon_windows_uninstall.ps1' + - 'powershell/migrate/falcon_windows_migrate.ps1' + pull_request: + paths: + - '.github/scripts/install-test-deps.sh' + - '.github/scripts/test-credential-handling.sh' + - '.github/scripts/test-curl-oauth-live.sh' + - '.github/scripts/test-powershell-credential-handling.ps1' + - '.github/workflows/credential_handling.yml' + - 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh' + - 'bash/install/falcon-linux-install.sh' + - 'bash/install/falcon-linux-uninstall.sh' + - 'bash/migrate/falcon-linux-migrate.sh' + - 'powershell/install/falcon_windows_install.ps1' + - 'powershell/install/falcon_windows_uninstall.ps1' + - 'powershell/migrate/falcon_windows_migrate.ps1' + +permissions: + contents: read + +jobs: + test: + name: Prevent credential exposure + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Run credential handling regression tests + run: bash .github/scripts/test-credential-handling.sh + + - name: Run PowerShell credential handling regression tests + shell: pwsh + run: ./.github/scripts/test-powershell-credential-handling.ps1 + + - name: Test curl OAuth behavior + run: sh .github/scripts/test-curl-oauth-live.sh + + curl-container-compatibility: + name: curl compatibility (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Oracle Linux 7 + image: oraclelinux:7 + - name: CentOS 7 + image: centos:7 + - name: Ubuntu 22.04 + image: ubuntu:22.04 + - name: Debian 12 + image: debian:12-slim + - name: Alpine 3.20 + image: alpine:3.20 + - name: AlmaLinux 9 + image: almalinux:9 + - name: Amazon Linux 2023 + image: amazonlinux:2023 + steps: + - name: Check out code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Test packaged curl + env: + CONTAINER_IMAGE: ${{ matrix.image }} + run: | + docker run --rm \ + --volume "$PWD:/repo:ro" \ + "$CONTAINER_IMAGE" \ + sh -c 'sh /repo/.github/scripts/install-test-deps.sh && sh /repo/.github/scripts/test-curl-oauth-live.sh' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75a16dc..daceba6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,8 +31,14 @@ jobs: zip -r ../../systemd_podman.zip * popd - # Create a list of files to upload - list_of_files=$(find . -type f \( -name "*.sh" -o -name "*.ps1" -o -name "*.zip" \)) + # Create a list of files to upload. Restrict the search to the + # directories that hold shipped artifacts, so CI helper scripts under + # .github/scripts and unit tests under bash/**/test/ stay out of the + # release assets and out of checksum.txt. + list_of_files=$( + find ./bash ./powershell ./systemd -type f \( -name "*.sh" -o -name "*.ps1" \) -not -path '*/test/*' + find . -maxdepth 1 -type f -name "*.zip" + ) # Create checksum.txt for file in $list_of_files; do diff --git a/bash/containers/falcon-container-sensor-pull/README.md b/bash/containers/falcon-container-sensor-pull/README.md index 01ed774..9482086 100644 --- a/bash/containers/falcon-container-sensor-pull/README.md +++ b/bash/containers/falcon-container-sensor-pull/README.md @@ -50,9 +50,9 @@ CrowdStrike now provides unified images that work across all regions: ## Security recommendations -### Use cURL version 7.55.0 or later +### Use cURL version 7.33.0 or later -We've identified a security concern related to cURL versions 7.54.1 and earlier. In these versions, request headers were set using the `-H` option, which allowed potential secrets to be exposed via the command line. In newer versions of cURL, versions 7.55.0 and later, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. **We recommend that you to upgrade cURL to version 7.55.0 or later**. If this is not possible, this script offers compatibility with the older method through the use of the `--allow-legacy-curl` optional command line flag. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and later accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, use `--allow-legacy-curl`. To check your version of cURL, run the following command: `curl --version` diff --git a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh index ecab4c0..f80bff0 100755 --- a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh +++ b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh @@ -1,4 +1,17 @@ #!/bin/bash + +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +unset FALCON_CLIENT_SECRET +FALCON_CLIENT_SECRET=$falcon_client_secret +unset falcon_client_secret + : <<'#DESCRIPTION#' File: falcon-container-sensor-pull.sh Description: Bash script to copy Falcon DaemonSet Sensor, Container Sensor, or Kubernetes Admission Controller images from CrowdStrike Container Registry. @@ -228,27 +241,39 @@ while [ $# != 0 ]; do shift done -# Check if curl is greater or equal to 7.55 -old_curl=$( +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi + +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" + minimum="7.33" - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -c >/dev/null 2>&1; then - echo 0 - else + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then echo 1 + else + echo 0 fi ) -# Old curl print warning message -if [ "$old_curl" -eq 0 ]; then - if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then echo """ -WARNING: Your version of curl does not support the ability to pass headers via stdin. -For security considerations, we strongly recommend upgrading to curl 7.55.0 or newer. +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. -To bypass this warning, set the optional flag --allow-legacy-curl +To accept this and continue, set the optional flag --allow-legacy-curl """ exit 1 fi @@ -287,13 +312,15 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - local token="$1" - set -- "$@" - if [ "$old_curl" -eq 0 ]; then - curl -s -L -H "Authorization: Bearer ${token}" "$@" + local token="$1" auth_config + shift + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$token") else - echo "Authorization: Bearer ${token}" | curl -s -L -H @- "$@" + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$token") fi + printf '%s\n' "$auth_config" | + curl -s -L --proto '=https' --proto-redir '=https' -K- "$@" } fetch_tags() { @@ -875,8 +902,7 @@ docker_api_token=$(echo "$raw_docker_api_token" | json_value "token") ART_PASSWORD=$(echo "$docker_api_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$ART_PASSWORD" ]; then - die "Failed to retrieve the CrowdStrike registry password. Response from API: -$raw_docker_api_token + die "Failed to retrieve the CrowdStrike registry password. Ensure the following: - Correct API Scopes assigned for sensor type: ${SENSOR_TYPE} diff --git a/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh new file mode 100644 index 0000000..0a79df7 --- /dev/null +++ b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh @@ -0,0 +1,149 @@ +#!/bin/sh +# Regression test for curl_command() argument handling. +# +# curl_command() takes the bearer token as its first parameter and must pass +# only the remaining parameters through to curl. If it does not consume that +# first parameter, curl receives the token as an extra positional parameter and +# treats it as a URL. +# +# This test loads curl_command() from the shipped script, replaces curl with a +# stub that records its argument vector, and checks that the token is never +# handed to curl as an argument. The token always travels on curl's +# configuration input, in both credential mechanisms that the script supports. +# +# Usage: sh test-curl-command.sh [path-to-falcon-container-sensor-pull.sh] + +# Re-run under every available shell. The script ships as POSIX sh and runs +# under whatever shell the operator has, so both dash and bash must pass. +if [ -z "${CURL_CMD_TEST_SHELL:-}" ]; then + overall=0 + for shell_bin in sh dash bash; do + command -v "$shell_bin" >/dev/null 2>&1 || continue + echo "=== shell under test: $shell_bin ===" + CURL_CMD_TEST_SHELL="$shell_bin" "$shell_bin" "$0" "$@" || overall=1 + echo + done + exit "$overall" +fi + +TEST_DIR=$(dirname "$0") +SCRIPT="${1:-$TEST_DIR/../falcon-container-sensor-pull.sh}" + +if [ ! -f "$SCRIPT" ]; then + echo "FATAL: cannot find script under test: $SCRIPT" >&2 + exit 1 +fi + +TMPDIR_TEST=$(mktemp -d) +ARGV_FILE="$TMPDIR_TEST/argv" +STDIN_FILE="$TMPDIR_TEST/stdin" +trap 'rm -rf "$TMPDIR_TEST"' EXIT + +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + echo " ok - $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo " NOT OK - $1" +} + +# A token shaped like the tokens the script really handles: dot-separated +# base64url segments. Every segment is short enough to be a valid DNS label, +# which is what makes an accidental positional argument reach the resolver. +TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJmYy1kZWFkYmVlZiJ9.c2lnbmF0dXJl" +URL="https://example.com/v2/tags/list" + +# Load the real curl_command() from the shipped script so the test tracks the +# code that ships, not a copy of it. +curl_command_source=$(sed -n '/^curl_command() {$/,/^}$/p' "$SCRIPT") +if [ -z "$curl_command_source" ]; then + echo "FATAL: could not extract curl_command() from $SCRIPT" >&2 + exit 1 +fi +eval "$curl_command_source" + +# Stub curl. Records each argument on its own line, and records stdin when the +# caller asks curl to read a header from it. +curl() { + : >"$ARGV_FILE" + : >"$STDIN_FILE" + for arg in "$@"; do + printf '%s\n' "$arg" >>"$ARGV_FILE" + if [ "$arg" = "-K-" ]; then + read_stdin=yes + fi + done + if [ "${read_stdin:-no}" = "yes" ]; then + cat >"$STDIN_FILE" + fi + read_stdin=no +} + +# The token must never be handed to curl in any argument. +assert_token_not_in_arguments() { + found=no + while IFS= read -r arg; do + case "$arg" in + *"$TOKEN"*) found=yes ;; + esac + done <"$ARGV_FILE" + + if [ "$found" = "yes" ]; then + fail "$1: token reached curl arguments" + echo " argv: $(tr '\n' ' ' <"$ARGV_FILE")" + else + pass "$1: token is absent from curl arguments" + fi +} + +assert_file_contains() { + # Use -- so that a pattern starting with a dash is not read as an option. + if grep -qF -- "$2" "$3"; then + pass "$1" + else + fail "$1" + echo " contents: $(tr '\n' ' ' <"$3")" + fi +} + +assert_url_passed_through() { + assert_file_contains "$1: request URL reached curl" "$URL" "$ARGV_FILE" +} + +# curl_command() picks its credential mechanism from curl_has_oauth2_bearer. +# Mode 1 uses the oauth2-bearer configuration key, which curl 7.33.0 added. +# Mode 0 is the fallback for older curl and sends a raw Authorization header. +# Both must keep the token on stdin and off the argument vector. +for mode in 1 0; do + # shellcheck disable=SC2034 # read by the curl_command body loaded with eval + curl_has_oauth2_bearer=$mode + if [ "$mode" -eq 1 ]; then + label="oauth2-bearer config" + expected="oauth2-bearer = \"$TOKEN\"" + else + label="raw header fallback" + expected="header = \"Authorization: Bearer $TOKEN\"" + fi + + echo "case: $label (curl_has_oauth2_bearer=$mode)" + curl_command "$TOKEN" "$URL" + assert_token_not_in_arguments "$label" + assert_url_passed_through "$label" + assert_file_contains "$label: token delivered over stdin" \ + "$expected" "$STDIN_FILE" + assert_file_contains "$label: curl reads the configuration from stdin" \ + "-K-" "$ARGV_FILE" + assert_file_contains "$label: request protocol restricted" \ + "--proto" "$ARGV_FILE" + assert_file_contains "$label: redirect protocol restricted" \ + "--proto-redir" "$ARGV_FILE" + echo +done + +echo "passed: $PASS failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/bash/install/README.md b/bash/install/README.md index 3ed5fa1..913fe41 100644 --- a/bash/install/README.md +++ b/bash/install/README.md @@ -7,9 +7,9 @@ environment variable. Consult the Environment Variables for each script for more ## Security Recommendations -### Use cURL version 7.55.0 or newer +### Use cURL version 7.33.0 or newer -We have identified a security concern related to cURL versions prior to 7.55, which required request headers to be set using the `-H` option, thus allowing potential secrets to be exposed via the command line. In newer versions of cURL, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. Although our script offers compatibility with the older method by allowing you to set the environment variable `ALLOW_LEGACY_CURL=true`, we strongly urge you to upgrade cURL if your environment permits. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and newer accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, set `ALLOW_LEGACY_CURL=true`. To check your version of cURL, run the following command: `curl --version` @@ -182,7 +182,7 @@ Other Options The path to download the falcon sensor to. - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. + To continue on a version of curl older than 7.33.0. - GET_ACCESS_TOKEN (default: false) Prints an access token and exits. diff --git a/bash/install/falcon-linux-install.sh b/bash/install/falcon-linux-install.sh index 19213f1..e8bd9d0 100755 --- a/bash/install/falcon-linux-install.sh +++ b/bash/install/falcon-linux-install.sh @@ -1,5 +1,21 @@ #!/bin/bash +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_provisioning_token=$FALCON_PROVISIONING_TOKEN +unset FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_PROVISIONING_TOKEN +FALCON_CLIENT_SECRET=$falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_PROVISIONING_TOKEN=$falcon_provisioning_token +unset falcon_client_secret falcon_access_token falcon_provisioning_token + print_usage() { cat </dev/null 2>&1; then + local_sha=$(sha256sum "$file" | awk '{ print $1 }') + else + local_sha=$(openssl dgst -sha256 "$file" | awk '{ print $NF }') + fi + if [ "$local_sha" != "$expected_sha" ]; then + rm -f "$file" + die "Downloaded sensor installer failed SHA-256 verification." + fi +} + cs_sensor_download() { local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer @@ -352,7 +382,7 @@ cs_sensor_download() { if echo "$existing_installers" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow sensor download (scope Sensor Download [read])" elif echo "$existing_installers" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sha_list=$(echo "$existing_installers" | json_value "sha256") @@ -376,6 +406,8 @@ cs_sensor_download() { handle_curl_error $? + verify_sha256 "$installer" "$sha" + echo "$installer" } @@ -467,9 +499,9 @@ aws_ssm_parameter() { token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") api_endpoint="AmazonSSM.GetParameters" - iam_role="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/)" - aws_my_region="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" - _security_credentials="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" + iam_role="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/)" + aws_my_region="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" + _security_credentials="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" access_key_id="$(echo "$_security_credentials" | grep AccessKeyId | sed -e 's/ "AccessKeyId" : "//' -e 's/",$//')" access_key_secret="$(echo "$_security_credentials" | grep SecretAccessKey | sed -e 's/ "SecretAccessKey" : "//' -e 's/",$//')" security_token="$(echo "$_security_credentials" | grep Token | sed -e 's/ "Token" : "//' -e 's/",$//')" @@ -507,23 +539,24 @@ EOF ) response=$( - curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ - -x "$proxy" \ - -H "Authorization: AWS4-HMAC-SHA256 \ - Credential=$access_key_id/$date/$aws_my_region/ssm/aws4_request, \ - SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, \ - Signature=$signature" \ - -H "x-amz-security-token: $security_token" \ - -H "x-amz-target: $api_endpoint" \ - -H "content-type: application/x-amz-json-1.1" \ - -d "$request_data" \ - -H "x-amz-date: $datetime" + { + printf 'header = "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/ssm/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=%s"\n' \ + "$access_key_id" "$date" "$aws_my_region" "$signature" + printf 'header = "x-amz-security-token: %s"\n' "$security_token" + printf 'header = "x-amz-target: %s"\n' "$api_endpoint" + printf 'header = "content-type: application/x-amz-json-1.1"\n' + printf 'header = "x-amz-date: %s"\n' "$datetime" + } | curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ + -x "$proxy" -K- \ + -d "$request_data" ) handle_curl_error $? - if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" - elif ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" + if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$' || + ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then + # The response body holds the decrypted parameter value, so report only + # the error message that AWS returns and never the body itself. + ssm_error=$(echo "$response" | json_value "message" 1) + die "Unexpected response from AWS SSM Parameter Store for parameter '$param_name'.${ssm_error:+ AWS reported:$ssm_error}" fi echo "$response" } @@ -613,31 +646,39 @@ cs_cloud() { esac } -# Check if curl is greater or equal to 7.55 -old_curl=$( - if ! command -v curl >/dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" + minimum="7.33" - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then echo 1 + else + echo 0 fi ) -# Old curl print warning message -if [ "$old_curl" -eq 0 ]; then - if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then echo """ -WARNING: Your version of curl does not support the ability to pass headers via stdin. -For security considerations, we strongly recommend upgrading to curl 7.55.0 or newer. +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. -To bypass this warning, set the environment variable ALLOW_LEGACY_CURL=true +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. + +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true """ exit 1 fi @@ -678,13 +719,14 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") fi + printf '%s\n' "$auth_config" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } check_aws_instance() { diff --git a/bash/install/falcon-linux-uninstall.sh b/bash/install/falcon-linux-uninstall.sh index 6b900fa..2644102 100755 --- a/bash/install/falcon-linux-uninstall.sh +++ b/bash/install/falcon-linux-uninstall.sh @@ -1,5 +1,21 @@ #!/bin/bash +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_maintenance_token=$FALCON_MAINTENANCE_TOKEN +unset FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_MAINTENANCE_TOKEN +FALCON_CLIENT_SECRET=$falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_MAINTENANCE_TOKEN=$falcon_maintenance_token +unset falcon_client_secret falcon_access_token falcon_maintenance_token + print_usage() { cat </dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi - - version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" - - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else - echo 1 - fi -) - curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") fi + printf '%s\n' "$auth_config" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } handle_curl_error() { @@ -288,6 +292,44 @@ die() { exit 1 } +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi + +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( + version=$(curl --version | head -n 1 | awk '{ print $2 }') + minimum="7.33" + + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then + echo 1 + else + echo 0 + fi +) + +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then + echo """ +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. + +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true +""" + exit 1 + fi +fi + aws_ssm_parameter() { local param_name="$1" @@ -299,9 +341,9 @@ aws_ssm_parameter() { token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") api_endpoint="AmazonSSM.GetParameters" - iam_role="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/)" - aws_my_region="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" - _security_credentials="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" + iam_role="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/)" + aws_my_region="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" + _security_credentials="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" access_key_id="$(echo "$_security_credentials" | grep AccessKeyId | sed -e 's/ "AccessKeyId" : "//' -e 's/",$//')" access_key_secret="$(echo "$_security_credentials" | grep SecretAccessKey | sed -e 's/ "SecretAccessKey" : "//' -e 's/",$//')" security_token="$(echo "$_security_credentials" | grep Token | sed -e 's/ "Token" : "//' -e 's/",$//')" @@ -339,23 +381,24 @@ EOF ) response=$( - curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ - -x "$proxy" \ - -H "Authorization: AWS4-HMAC-SHA256 \ - Credential=$access_key_id/$date/$aws_my_region/ssm/aws4_request, \ - SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, \ - Signature=$signature" \ - -H "x-amz-security-token: $security_token" \ - -H "x-amz-target: $api_endpoint" \ - -H "content-type: application/x-amz-json-1.1" \ - -d "$request_data" \ - -H "x-amz-date: $datetime" + { + printf 'header = "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/ssm/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=%s"\n' \ + "$access_key_id" "$date" "$aws_my_region" "$signature" + printf 'header = "x-amz-security-token: %s"\n' "$security_token" + printf 'header = "x-amz-target: %s"\n' "$api_endpoint" + printf 'header = "content-type: application/x-amz-json-1.1"\n' + printf 'header = "x-amz-date: %s"\n' "$datetime" + } | curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ + -x "$proxy" -K- \ + -d "$request_data" ) handle_curl_error $? - if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" - elif ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" + if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$' || + ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then + # The response body holds the decrypted parameter value, so report only + # the error message that AWS returns and never the body itself. + ssm_error=$(echo "$response" | json_value "message" 1) + die "Unexpected response from AWS SSM Parameter Store for parameter '$param_name'.${ssm_error:+ AWS reported:$ssm_error}" fi echo "$response" } diff --git a/bash/migrate/README.md b/bash/migrate/README.md index 65884e4..8bc2fba 100644 --- a/bash/migrate/README.md +++ b/bash/migrate/README.md @@ -4,9 +4,9 @@ Bash script to migrate Falcon sensor from one CID to another through the Falcon ## Security Recommendations -### Use cURL version 7.55.0 or newer +### Use cURL version 7.33.0 or newer -We have identified a security concern related to cURL versions prior to 7.55, which required request headers to be set using the `-H` option, thus allowing potential secrets to be exposed via the command line. In newer versions of cURL, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. Although our script offers compatibility with the older method by allowing you to set the environment variable `ALLOW_LEGACY_CURL=true`, we strongly urge you to upgrade cURL if your environment permits. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and newer accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, set `ALLOW_LEGACY_CURL=true`. To check your version of cURL, run the following command: `curl --version` @@ -184,7 +184,7 @@ Other Options Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. + To continue on a version of curl older than 7.33.0. - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making diff --git a/bash/migrate/falcon-linux-migrate.sh b/bash/migrate/falcon-linux-migrate.sh index 258ecc7..930f314 100755 --- a/bash/migrate/falcon-linux-migrate.sh +++ b/bash/migrate/falcon-linux-migrate.sh @@ -1,4 +1,24 @@ #!/bin/bash + +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +old_falcon_client_secret=$OLD_FALCON_CLIENT_SECRET +new_falcon_client_secret=$NEW_FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_maintenance_token=$FALCON_MAINTENANCE_TOKEN +falcon_provisioning_token=$FALCON_PROVISIONING_TOKEN +unset OLD_FALCON_CLIENT_SECRET NEW_FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_MAINTENANCE_TOKEN FALCON_PROVISIONING_TOKEN +OLD_FALCON_CLIENT_SECRET=$old_falcon_client_secret +NEW_FALCON_CLIENT_SECRET=$new_falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_MAINTENANCE_TOKEN=$falcon_maintenance_token +FALCON_PROVISIONING_TOKEN=$falcon_provisioning_token +unset old_falcon_client_secret new_falcon_client_secret falcon_access_token falcon_maintenance_token falcon_provisioning_token # # Bash script to migrate Falcon sensor to another falcon CID. # @@ -111,7 +131,7 @@ Other Options Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. + To continue on a version of curl older than 7.33.0. - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making @@ -198,31 +218,54 @@ uninstall_sensor() { } # Shared functions -old_curl=$( - if ! command -v curl >/dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" + minimum="7.33" - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then echo 1 + else + echo 0 fi ) +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then + echo """ +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. + +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true +""" + exit 1 + fi +fi + curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") fi + printf '%s\n' "$auth_config" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } handle_curl_error() { @@ -472,7 +515,7 @@ get_maintenance_token() { die "Retrieved empty maintenance token from API." fi else - die "Failed to retrieve maintenance token. Response: $response" + die "Failed to retrieve a maintenance token from the Falcon API." fi } @@ -574,7 +617,7 @@ cs_sensor_policy_version() { if echo "$sensor_update_policy" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow access to sensor update policies (scope Sensor update policies [read])" elif echo "$sensor_update_policy" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sensor_update_versions=$(echo "$sensor_update_policy" | json_value "sensor_version") @@ -598,6 +641,20 @@ cs_sensor_policy_version() { IFS=$oldIFS } +verify_sha256() { + local file="$1" expected_sha="$2" local_sha + + if command -v sha256sum >/dev/null 2>&1; then + local_sha=$(sha256sum "$file" | awk '{ print $1 }') + else + local_sha=$(openssl dgst -sha256 "$file" | awk '{ print $NF }') + fi + if [ "$local_sha" != "$expected_sha" ]; then + rm -f "$file" + die "Downloaded sensor installer failed SHA-256 verification." + fi +} + cs_sensor_download() { local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer @@ -621,7 +678,7 @@ cs_sensor_download() { if echo "$existing_installers" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow sensor download (scope Sensor Download [read])" elif echo "$existing_installers" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sha_list=$(echo "$existing_installers" | json_value "sha256") @@ -645,6 +702,8 @@ cs_sensor_download() { handle_curl_error $? + verify_sha256 "$installer" "$sha" + echo "$installer" } @@ -860,7 +919,7 @@ get_falcon_tags() { if echo "$response" | grep "authorization failed" >/dev/null; then die "Access denied: Please make sure your Falcon API credentials allow access to host data (scope Host [read])" elif echo "$response" | grep "invalid bearer token" >/dev/null; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi # Extract tags from response diff --git a/powershell/install/falcon_windows_install.ps1 b/powershell/install/falcon_windows_install.ps1 index e1ea7ff..7c38d39 100755 --- a/powershell/install/falcon_windows_install.ps1 +++ b/powershell/install/falcon_windows_install.ps1 @@ -135,6 +135,8 @@ param( [string] $UserAgent ) begin { + Set-PSDebug -Off + if ($PSVersionTable.PSVersion -lt '3.0') { throw "This script requires a miniumum PowerShell 3.0" } @@ -214,7 +216,6 @@ begin { try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.' @@ -597,7 +598,7 @@ process { # Begin installation Write-FalconLog 'Installer' 'Installing Falcon Sensor...' - Write-FalconLog 'StartProcess' "Starting installer with parameters: '$InstallParams'" + Write-FalconLog 'StartProcess' 'Starting installer; command-line parameters omitted from the log because they may contain sensitive values' try { $process = (Start-Process -FilePath $LocalFile -ArgumentList $InstallParams -PassThru -ErrorAction SilentlyContinue) Write-FalconLog 'StartProcess' "Started '$LocalFile' ($($process.Id))" diff --git a/powershell/install/falcon_windows_uninstall.ps1 b/powershell/install/falcon_windows_uninstall.ps1 index a7fb5b2..916bb70 100755 --- a/powershell/install/falcon_windows_uninstall.ps1 +++ b/powershell/install/falcon_windows_uninstall.ps1 @@ -115,6 +115,8 @@ param( [string] $UserAgent ) begin { + Set-PSDebug -Off + if ($FalconAccessToken) { if ($FalconCloud -eq "autodiscover") { @@ -200,7 +202,6 @@ begin { try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $Message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.' @@ -529,7 +530,6 @@ process { try { $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Body $bodyJson -MaximumRedirection 0 $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'GetToken - $content:' if ($content.errors) { $Message = 'Failed to retrieve maintenance token: ' @@ -539,7 +539,7 @@ process { } else { $MaintenanceToken = $content.resources[0].uninstall_token - Write-FalconLog 'GetToken' "Retrieved maintenance token: $MaintenanceToken" + Write-FalconLog 'GetToken' 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" } } @@ -576,9 +576,8 @@ process { if ($UninstallTool -eq 'standalone') { # Check if /uninstall parameter is present if ($UninstallParams -match '/?uninstall') { - $OriginalParams = $UninstallParams $UninstallParams = $UninstallParams -replace '/?uninstall\s*', '' -replace '^\s+|\s+$', '' - Write-FalconLog 'ParamValidation' "Removed '/uninstall' parameter for standalone uninstaller. Original: '$OriginalParams', Modified: '$UninstallParams'" + Write-FalconLog 'ParamValidation' "Removed '/uninstall' parameter for standalone uninstaller; parameter values omitted from the log" } # Ensure we have at least /quiet parameter @@ -590,7 +589,7 @@ process { # Begin uninstallation Write-FalconLog 'Uninstaller' 'Uninstalling the Falcon Sensor...' - Write-FalconLog 'StartProcess' "Starting uninstaller with parameters: '$UninstallParams'" + Write-FalconLog 'StartProcess' 'Starting uninstaller; command-line parameters omitted from the log because they may contain sensitive values' $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog 'StartProcess' "Started '$UninstallerPath' ($UninstallerProcessId)" diff --git a/powershell/migrate/falcon_windows_migrate.ps1 b/powershell/migrate/falcon_windows_migrate.ps1 index 2f196ac..909a552 100644 --- a/powershell/migrate/falcon_windows_migrate.ps1 +++ b/powershell/migrate/falcon_windows_migrate.ps1 @@ -135,6 +135,8 @@ param( [string] $UserAgent ) +Set-PSDebug -Off + function Write-RecoveryCsv { param ( @@ -324,7 +326,6 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Headers $oldCloudHeaders -Body $bodyJson -MaximumRedirection 0 $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'GetToken - $content:' if ($content.errors) { $Message = 'Failed to retrieve maintenance token: ' @@ -334,7 +335,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst } else { $MaintenanceToken = $content.resources[0].uninstall_token - Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Retrieved maintenance token: $MaintenanceToken" + Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" } } @@ -369,7 +370,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst # Begin uninstallation Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Uninstalling Falcon Sensor...' - Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Starting uninstaller with parameters: '$UninstallParams'" + Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Starting uninstaller; command-line parameters omitted from the log because they may contain sensitive values' $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Started '$UninstallerPath' ($UninstallerProcessId)" @@ -579,7 +580,7 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP # Begin installation Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Installing Falcon Sensor..." - Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Starting installer '$LocalFile' with parameters '$InstallParams'" + Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Starting installer '$LocalFile'; command-line parameters omitted from the log because they may contain sensitive values" $process = (Start-Process -FilePath $LocalFile -ArgumentList $InstallParams -PassThru -ErrorAction SilentlyContinue) Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Started '$LocalFile' ($($process.Id))" @@ -1008,7 +1009,6 @@ function Invoke-FalconAuth([hashtable] $WebRequestParams, [string] $BaseUrl, [ha try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.'