Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [1.0 Introduction](#10-introduction)
- [2.0 How it works](#20-how-it-works)
- [2.1 ECS Exec is not used](#21-ecs-exec-is-not-used)
- [3.0 Setup](#30-setup)
- [3.1 Environment](#31-environment)
- [3.2 Registration](#32-registration)
Expand Down Expand Up @@ -34,6 +35,20 @@ Each requested _task run_ is placed on the **Node**.
Which means, anything described inside that task will be running on **Node** (self-hosted/external instance).
Only, _task definition_ will live on _AWS ECS_.

### 2.1 ECS Exec is not used

Workflow tasks are never launched with `enableExecuteCommand`, so ECS Exec is
unused on a runner. AWS's `ecs-anywhere-install.sh` nevertheless stages the SSM
session binaries into `/var/lib/ecs/deps/execute-command` on every install — the
call is unconditional and the script offers no flag to skip it — which leaves
unused binaries on disk for vulnerability scanners to flag.

Registration therefore skips that step and removes the directory if anything was
staged anyway. This is not configurable: there is no supported setup in which the
runner needs those binaries. The ECS agent treats them as optional and simply
stops advertising the `ecs.capability.execute-command` attribute when they are
absent.

## 3.0 Setup

> **IMPORTANT:**
Expand Down
82 changes: 81 additions & 1 deletion main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ readonly SG_DOCKER_NETWORK="sg-net"
ECS_CONFIG_DIR="${ECS_CONFIG_DIR:=/etc/ecs}"
ECS_LOG_DIR="${ECS_LOG_DIR:=/var/log/ecs}"
ECS_DATA_DIR="${ECS_DATA_DIR:=/var/lib/ecs/data}"
ECS_EXEC_DEPS_DIR="${ECS_EXEC_DEPS_DIR:=/var/lib/ecs/deps/execute-command}"
REGISTRATION_DIR="${REGISTRATION_DIR:=/var/log/registration}"
readonly ECS_CONFIG_DIR ECS_LOG_DIR ECS_DATA_DIR REGISTRATION_DIR
readonly ECS_CONFIG_DIR ECS_LOG_DIR ECS_DATA_DIR ECS_EXEC_DEPS_DIR REGISTRATION_DIR

# diagnostics
SG_DIAGNOSTIC_DIR="${SG_DIAGNOSTIC_DIR:=/var/lib/sg-runner}"
Expand Down Expand Up @@ -734,6 +735,80 @@ EOF
}
#}}}: configure_http_proxy

# StackGuardian never launches ECS tasks with enableExecuteCommand, so ECS Exec
# is unused on runners. AWS's ecs-anywhere-install.sh stages the SSM session
# binaries under ECS_EXEC_DEPS_DIR regardless: its exec-setup call is
# unconditional and the script exposes no flag to skip it. That leaves unused
# binaries on disk which trip vulnerability scanners, e.g. CVE-2026-71556 in the
# go-git version vendored by amazon-ssm-agent 3.3.4624.0.
#
# The ECS agent treats these dependencies as optional: when the directory is
# absent it starts normally and simply stops advertising the
# ecs.capability.execute-command attribute (see appendExecCapabilities in
# amazon-ecs-agent), which we never rely on.

disable_ecs_exec_setup() { #{{{
# Best effort: neuter the installer's `exec-setup` call so the SSM binaries
# are never downloaded. remove_ecs_exec_deps is the backstop if this misses.
local script="$1"

if ! grep -qx 'exec-setup' "$script"; then
debug "No exec-setup call found in" "$(basename "$script")" "- skipping patch."
return 0
fi

# Write-and-move rather than `sed -i`: the in-place flag is not portable.
if sed 's/^exec-setup$/: # exec-setup disabled: ECS Exec is unused/' \
"$script" >"${script}.patched" && mv "${script}.patched" "$script"; then
debug "Disabled ECS Exec dependency staging in" "$(basename "$script")"
else
rm -f "${script}.patched"
debug "Could not patch" "$(basename "$script")" "- relying on cleanup."
fi

# debug() is a no-op returning non-zero unless --debug is set; never let that
# become this function's exit status.
return 0
}
#}}}: disable_ecs_exec_setup

is_ecs_exec_deps_path() { #{{{
# Guard for the root-run `rm -rf` in remove_ecs_exec_deps. ECS_EXEC_DEPS_DIR
# is overridable for testing, which makes it the one place in this script
# where an env var supplies a whole deletion path rather than a fixed literal.
# Accept only an absolute path that actually names a deps directory, so a
# stray "/" or "/etc" in the environment can never reach rm.
#
# Kept as a pure predicate so the dangerous inputs are unit-testable without
# any test ever pointing rm at them. A trailing slash is rejected too: the
# refusal is logged and harmless, unlike the alternative.
case "${1:-}" in
/*/execute-command) return 0 ;;
*) return 1 ;;
esac
}
#}}}: is_ecs_exec_deps_path

remove_ecs_exec_deps() { #{{{
# Authoritative cleanup: drop whatever exec-setup managed to stage.
if ! is_ecs_exec_deps_path "$ECS_EXEC_DEPS_DIR"; then
debug "Refusing to remove unexpected ECS_EXEC_DEPS_DIR:" "$ECS_EXEC_DEPS_DIR"
return 0
fi

[[ -d "$ECS_EXEC_DEPS_DIR" ]] || return 0

if rm -rf "$ECS_EXEC_DEPS_DIR"; then
Comment thread
hllvc marked this conversation as resolved.
debug "Removed unused ECS Exec dependencies:" "$ECS_EXEC_DEPS_DIR"
else
debug "Failed to remove ECS Exec dependencies:" "$ECS_EXEC_DEPS_DIR"
fi

# Hardening only: a failure here must not fail an otherwise good registration.
return 0
}
#}}}: remove_ecs_exec_deps

#}}}: Local configuration

#{{{ Registration / deregistration
Expand Down Expand Up @@ -830,6 +905,9 @@ register_instance() { #{{{
rm -f "$ecs_install_script"
die "Downloaded script appears invalid" "missing bash shebang"
fi

disable_ecs_exec_setup "$ecs_install_script"

spinner_msg "Downloading support files" 0

check_systemctl_ecs_status
Expand Down Expand Up @@ -869,6 +947,8 @@ register_instance() { #{{{
done &
spinner "$!" "Verifying registration of this runner"

remove_ecs_exec_deps

# setup_cron
save_registration_details
}
Expand Down
6 changes: 4 additions & 2 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ test/
- **Path overrides via env.** Every path it writes to is overridable:
`LOG_FILE`, `SG_DIAGNOSTIC_DIR` (`SG_DIAGNOSTIC_FILE` /
`SG_DIAGNOSTIC_TMP_FILE` derive from it), `ECS_CONFIG_DIR`, `ECS_LOG_DIR`,
`ECS_DATA_DIR`, `REGISTRATION_DIR`. `load.bash` points all of these at a
per-test temp dir, so a test never touches real system paths.
`ECS_DATA_DIR`, `ECS_EXEC_DEPS_DIR`, `REGISTRATION_DIR`. `load.bash` points
all of these at a per-test temp dir, so a test never touches real system
paths. This matters most for `ECS_EXEC_DEPS_DIR`: `remove_ecs_exec_deps`
runs `rm -rf` on it.
- **Root bypass.** `SG_SKIP_ROOT_CHECK=true` makes `is_root()` succeed without
root. `load.bash` sets it.
- **API + debug.** `SG_BASE_API` overrides the API base; `LOG_DEBUG=true`
Expand Down
50 changes: 50 additions & 0 deletions test/fixtures/ecs/ecs-anywhere-install.sh.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash
# Trimmed stand-in for AWS's ecs-anywhere-install-latest.sh, preserving the
# parts disable_ecs_exec_setup cares about: the exec-setup function definition,
# its helper functions, and the unconditional top-level `exec-setup` call in the
# script's main flow. Sourced facts (function names, the hardcoded
# BINARY_VERSION, the call order) mirror the upstream script.

set -e

SKIP_REGISTRATION=false
DOCKER_SOURCE=""

ok() { echo "ok"; }

install-ssm-agent() { echo "install-ssm-agent"; }

install-docker() { echo "install-docker $1"; }

exec-setup() {
find-copy-certs-exec
download-ssm-binaries-exec
}

find-copy-certs-exec() {
CERTS_PATH="/var/lib/ecs/deps/execute-command/certs"
echo "Copying certs for exec feature to ${CERTS_PATH}"
ok
}

download-ssm-binaries-exec() {
BINARY_VERSION="3.3.4624.0"
BINARY_PATH="/var/lib/ecs/deps/execute-command/bin/${BINARY_VERSION}"
echo "Downloading SSM binaries for exec feature to ${BINARY_PATH}"
ok
}

install-ecs-agent() { echo "install-ecs-agent"; }

wait-agent-start() { echo "wait-agent-start"; }

show-license() { echo "show-license"; }

if ! $SKIP_REGISTRATION; then
install-ssm-agent
fi
install-docker "$DOCKER_SOURCE"
exec-setup
install-ecs-agent
wait-agent-start
show-license
1 change: 1 addition & 0 deletions test/helpers/load.bash
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export SG_DIAGNOSTIC_DIR="${SG_TEST_TMPDIR}/sg-runner"
export ECS_CONFIG_DIR="${SG_TEST_TMPDIR}/etc-ecs"
export ECS_LOG_DIR="${SG_TEST_TMPDIR}/var-log-ecs"
export ECS_DATA_DIR="${SG_TEST_TMPDIR}/var-lib-ecs-data"
export ECS_EXEC_DEPS_DIR="${SG_TEST_TMPDIR}/var-lib-ecs-deps/execute-command"
export REGISTRATION_DIR="${SG_TEST_TMPDIR}/registration"

# Bypass the root requirement.
Expand Down
Loading