Skip to content
Merged
8 changes: 7 additions & 1 deletion terraform/eks/daemon/liscsi/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,13 @@ resource "null_resource" "validator" {
kubectl get svc -n kube-system -l app.kubernetes.io/name=ec2-instance-store-plugin 2>/dev/null || echo "No LIS CSI service found"
echo "=== Running go test ==="
cd ../../../..
go test ${var.test_dir} -timeout 1h -eksClusterName=${aws_eks_cluster.this.name} -computeType=EKS -v -eksDeploymentStrategy=DAEMON -instanceId=${data.aws_instance.eks_node_detail.instance_id}
i=0
while [ $i -lt 3 ]; do
i=$((i+1))
go test ${var.test_dir} -timeout 1h -eksClusterName=${aws_eks_cluster.this.name} -computeType=EKS -v -eksDeploymentStrategy=DAEMON -instanceId=${data.aws_instance.eks_node_detail.instance_id} && exit 0
sleep 60
done
exit 1
EOT
}
}
5 changes: 2 additions & 3 deletions test/app_signals_service_events/metrics_routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@ func TestAppSignalsMetricsRouting(t *testing.T) {

// OTLP validation: otlphttpexporter debug log shows request to monitoring endpoint
t.Run("service_events_sent_to_otlp_monitoring_endpoint", func(t *testing.T) {
assert.Contains(t, agentLogStr,
`"Preparing to make HTTP request","url":"https://monitoring.us-west-2.amazonaws.com/v1/metrics"`,
"otlphttpexporter should log HTTP request to monitoring endpoint for ServiceEvents metrics")
assert.Regexp(t, `"Preparing to make HTTP request".*"url":"https://monitoring\.us-west-2\.amazonaws\.com/v1/metrics"`, agentLogStr,
"otlphttpexporter should log an HTTP request to the monitoring endpoint for ServiceEvents metrics")
})

// OTLP validation: query the PromQL API to confirm the metric was actually ingested
Expand Down
4 changes: 1 addition & 3 deletions test/app_signals_service_events/service_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,7 @@ func TestAppSignalsOnPremCredentialsStartup(t *testing.T) {

agentLog := common.ReadAgentLogfile(common.AgentLogFile)
assert.NotContains(t, agentLog, "could not retrieve credential provider",
"sigv4auth should not eagerly resolve credentials via IMDS when a credentials file is provided")
assert.NotContains(t, agentLog, "no EC2 IMDS role found",
"sigv4auth should use the provided credentials file instead of requiring IMDS")
"sigv4auth should resolve credentials from the file, not fail over to IMDS")

assertAgentStable(t,
"agent should start in onPrem mode with a credentials file even when IMDS is disabled")
Expand Down
118 changes: 118 additions & 0 deletions test/assume_role/assume_role_scan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

//go:build !windows

package assume_role

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/aws/amazon-cloudwatch-agent-test/environment"
)

const (
testAccountID = "111122223333"
testInstanceArn = "arn:aws:ec2:us-west-2:111122223333:instance/i-0abcdef1234567890"
)

// v2Log mirrors the aws-sdk-go-v2 / smithy RequestResponseLogger output, which emits
// "D! Request\n" followed by httputil.DumpRequestOut and has no trailing marker.
const v2Log = `2026/07/03 18:05:10 D! Request
POST / HTTP/1.1
Host: sts.us-west-2.amazonaws.com
User-Agent: aws-sdk-go-v2/1.41.5 os/linux lang/go
Content-Length: 199
Authorization: AWS4-HMAC-SHA256 Credential=AKIA/20260703/us-west-2/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-security-token;x-amz-source-account;x-amz-source-arn, Signature=abc
Content-Type: application/x-www-form-urlencoded; charset=utf-8
X-Amz-Date: 20260703T180510Z
X-Amz-Security-Token: token
X-Amz-Source-Account: 111122223333
X-Amz-Source-Arn: arn:aws:ec2:us-west-2:111122223333:instance/i-0abcdef1234567890
Accept-Encoding: gzip

Action=AssumeRole&DurationSeconds=900&RoleArn=arn%3Aaws%3Aiam%3A%3A111122223333%3Arole%2Fcwa-integ-assume-role-all_context_keys&RoleSessionName=123&Version=2011-06-15
2026/07/03 18:05:10 D! Response
HTTP/1.1 200 OK
`

// v1Log mirrors the legacy aws-sdk-go v1 wire log with POST-SIGN start and dashed end markers.
const v1Log = `2026/07/03 18:05:10 I! ---[ REQUEST POST-SIGN ]-----------------------------
POST / HTTP/1.1
Host: sts.us-west-2.amazonaws.com
X-Amz-Source-Account: 111122223333
X-Amz-Source-Arn: arn:aws:ec2:us-west-2:111122223333:instance/i-0abcdef1234567890

Action=AssumeRole&Version=2011-06-15
-----------------------------------------------------
`

func TestScanForConfusedDeputyHeaders(t *testing.T) {
metadata = &environment.MetaData{
AccountId: testAccountID,
InstanceArn: testInstanceArn,
}

cases := []struct {
name string
log string
want bool
}{
{name: "sdk v2 format", log: v2Log, want: true},
{name: "sdk v1 format", log: v1Log, want: true},
{
name: "assume role request missing headers",
log: `2026/07/03 18:05:10 D! Request
POST / HTTP/1.1
Host: sts.us-west-2.amazonaws.com

Action=AssumeRole&Version=2011-06-15
`,
want: false,
},
{
name: "headers present but not an assume role request",
log: `2026/07/03 18:05:10 D! Request
POST / HTTP/1.1
Host: monitoring.us-west-2.amazonaws.com
X-Amz-Source-Account: 111122223333
X-Amz-Source-Arn: arn:aws:ec2:us-west-2:111122223333:instance/i-0abcdef1234567890

Action=PutMetricData&Version=2010-08-01
`,
want: false,
},
{
name: "wrong account and arn",
log: `2026/07/03 18:05:10 D! Request
POST / HTTP/1.1
X-Amz-Source-Account: 123456789012
X-Amz-Source-Arn: arn:aws:ec2:us-west-2:123456789012:instance/i-1234567890abcdef0

Action=AssumeRole&Version=2011-06-15
`,
want: false,
},
{name: "no debug logs", log: "2026/07/03 18:05:10 I! Agent started\n", want: false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, scanForConfusedDeputyHeaders(strings.NewReader(tc.log)))
})
}
}

func TestBlockStartDetection(t *testing.T) {
assert.True(t, isRequestBlockStart("2026/07/03 18:05:10 D! Request"))
assert.True(t, isRequestBlockStart("... ---[ REQUEST POST-SIGN ]-----------------------------"))
assert.False(t, isRequestBlockStart("2026/07/03 18:05:10 D! Response"))
assert.False(t, isRequestBlockStart("Action=AssumeRole&Version=2011-06-15"))

assert.True(t, isHTTPDebugBlockStart("2026/07/03 18:05:10 D! Response"))
assert.True(t, isHTTPDebugBlockStart("2026/07/03 18:05:10 D! Request"))
assert.False(t, isHTTPDebugBlockStart("X-Amz-Source-Account: 111122223333"))
}
156 changes: 107 additions & 49 deletions test/assume_role/assume_role_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package assume_role
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
Expand Down Expand Up @@ -401,8 +402,34 @@ func (t *ConfusedDeputyAssumeRoleTestRunner) validateAccessDenied() status.TestR
return testResult
}

// validateFoundConfusedDeputyHeaders checks that the agent used confued deputy headers in the STS assume role calls
// using the agent's logs
// validateFoundConfusedDeputyHeaders checks that the agent used confused deputy headers in the STS assume role calls using the agent's logs.
//
// The check works by isolating the HTTP request debug blocks emitted by the AWS SDK and looking for a block that
// (a) is an STS AssumeRole request and (b) carries both confused deputy headers. It supports both SDK request-log
// formats:
//
// aws-sdk-go v1 (marker-delimited):
// ---[ REQUEST POST-SIGN ]-----------------------------
// POST / HTTP/1.1
// ...
// X-Amz-Source-Account: 0123456789012
// X-Amz-Source-Arn: arn:aws:ec2:...:instance/i-...
// ...
// Action=AssumeRole&...&Version=2011-06-15
// -----------------------------------------------------
//
// aws-sdk-go-v2 / smithy (smithyhttp.RequestResponseLogger via httputil.DumpRequestOut):
// <ts> D! Request
// POST / HTTP/1.1
// Host: sts.us-west-2.amazonaws.com
// ...
// X-Amz-Source-Account: 0123456789012
// X-Amz-Source-Arn: arn:aws:ec2:...:instance/i-...
// ...
// Action=AssumeRole&...&Version=2011-06-15
//
// The v2 format has no explicit end marker, so request blocks are delimited by the start of the next
// request/response debug block (or end of file).
func (t *ConfusedDeputyAssumeRoleTestRunner) validateFoundConfusedDeputyHeaders() status.TestResult {

testResult := status.TestResult{
Expand All @@ -417,73 +444,104 @@ func (t *ConfusedDeputyAssumeRoleTestRunner) validateFoundConfusedDeputyHeaders(
}
defer file.Close()

scanner := bufio.NewScanner(file)
if scanForConfusedDeputyHeaders(file) {
log.Println("Found confused deputy headers in the HTTP debug log")
testResult.Status = status.SUCCESSFUL
}

return testResult
}

// scanForConfusedDeputyHeaders scans an AWS SDK debug log stream for an STS AssumeRole request block that carries
// both confused deputy headers.
func scanForConfusedDeputyHeaders(r io.Reader) bool {
scanner := bufio.NewScanner(r)

inHttpDebug := false
isStsAssumeRoleRequest := false
inRequestBlock := false
httpDebugLog := []string{}

// Example HTTP debug log
//
// ---[ REQUEST POST-SIGN ]-----------------------------
// POST / HTTP/1.1
// Host: sts.us-west-2.amazonaws.com
// User-Agent: aws-sdk-go/1.48.6 (go1.22.11; linux; arm64)
// Content-Length: 199
// Authorization: AWS4-HMAC-SHA256 Credential=<snip>/<snip>/us-west-2/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-security-token;x-amz-source-account;x-amz-source-arn, Signature=<snip>
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
// X-Amz-Date: 20250129T170140Z
// X-Amz-Security-Token: <token>
// X-Amz-Source-Account: 0123456789012
// X-Amz-Source-Arn: arn:aws:ec2:us-west-2:123456789012:instance/i-1234567890abcdef0
// Accept-Encoding: gzip
//
// Action=AssumeRole&DurationSeconds=900&RoleArn=arn%3Aaws%3Aiam%3A%3A506463145083%3Arole%2Fcwa-integ-assume-role-5be6d1574e9843bb-all_context_keys&RoleSessionName=1738170071781577224&Version=2011-06-15
// -----------------------------------------------------
// evaluate returns true when the accumulated block is an STS AssumeRole request that carries both
// confused deputy headers.
evaluate := func(block []string) bool {
isStsAssumeRoleRequest := false
for _, l := range block {
if strings.Contains(l, "Action=AssumeRole") {
isStsAssumeRoleRequest = true
break
}
}
return isStsAssumeRoleRequest && checkForConfusedDeputyHeaders(block)
}

for scanner.Scan() {
line := scanner.Text()

// Look for the start of an HTTP request debug log
if strings.Contains(line, "---[ REQUEST POST-SIGN ]-----------------------------") {
inHttpDebug = true
httpDebugLog = []string{}
isStsAssumeRoleRequest = false
continue
}
// The start of any new HTTP request/response debug block terminates the block currently being
// accumulated. Evaluate it before (re)starting.
if isHTTPDebugBlockStart(line) {
if inRequestBlock && evaluate(httpDebugLog) {
return true
}

// Ignore anything thats not part of an HTTP request debug log
if !inHttpDebug {
// Only accumulate request blocks; response blocks are irrelevant here.
inRequestBlock = isRequestBlockStart(line)
httpDebugLog = httpDebugLog[:0]
if inRequestBlock {
httpDebugLog = append(httpDebugLog, line)
}
continue
}

httpDebugLog = append(httpDebugLog, line)

if strings.Contains(line, "Action=AssumeRole") {
isStsAssumeRoleRequest = true
}

// Look for the end of an HTTP request debug log
if strings.Contains(line, "-----------------------------------------------------") {

if isStsAssumeRoleRequest && checkForConfusedDeputyHeaders(httpDebugLog) {
log.Println("Found confused deputy headers in the HTTP debug log")
testResult.Status = status.SUCCESSFUL
return testResult
// The v1 format has an explicit end marker; treat it as a block terminator too.
if inRequestBlock && strings.Contains(line, "-----------------------------------------------------") {
if evaluate(httpDebugLog) {
return true
}
inRequestBlock = false
httpDebugLog = httpDebugLog[:0]
continue
}

// Reset the search
inHttpDebug = false
isStsAssumeRoleRequest = false
httpDebugLog = []string{}
if inRequestBlock {
httpDebugLog = append(httpDebugLog, line)
}
}

// The v2 format has no end marker, so the final block is only terminated by EOF.
if inRequestBlock && evaluate(httpDebugLog) {
return true
}

if err := scanner.Err(); err != nil {
log.Printf("Error reading file: %v\n", err)
}

return testResult
return false
}

// isRequestBlockStart reports whether the log line marks the start of an HTTP request debug block, for either the
// aws-sdk-go v1 wire log or the aws-sdk-go-v2 / smithy RequestResponseLogger output.
func isRequestBlockStart(line string) bool {
// aws-sdk-go v1
if strings.Contains(line, "---[ REQUEST POST-SIGN ]---") {
return true
}
// aws-sdk-go-v2 / smithy: the logger emits "Request\n<dumped request>", so the first line ends with "Request".
return strings.HasSuffix(strings.TrimSpace(line), "D! Request")
}

// isHTTPDebugBlockStart reports whether the log line marks the start of any HTTP request or response debug block. It
// is used to delimit request blocks in the marker-less aws-sdk-go-v2 format.
func isHTTPDebugBlockStart(line string) bool {
if isRequestBlockStart(line) {
return true
}
// aws-sdk-go v1 response marker
if strings.Contains(line, "---[ RESPONSE ]---") {
return true
}
// aws-sdk-go-v2 / smithy response block
return strings.HasSuffix(strings.TrimSpace(line), "D! Response")
}

// checkForConfusedDeputyHeaders checks for the presence of the confused deputy headers in the HTTP debug log
Expand Down
2 changes: 1 addition & 1 deletion test/credential_chain/common_config_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (t *CommonConfigTestRunner) Validate() status.TestGroupResult {
return util.ValidateCredentialTest(t.GetTestName(), util.ExpectedResults{
Namespace: util.SharedTestNamespace,
MetricName: util.MetricNameCpuUsageActive,
CredentialProviderName: util.ProviderNameSharedCredentials,
CredentialProviderName: util.ProviderNameSharedConfig,
AccessKeyID: t.accessKeyID,
}, metadata)
}
Expand Down
3 changes: 1 addition & 2 deletions test/credential_chain/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const (
UserCWAgentHomeDir = "/home/cwagent"
AwsCredentialsPath = ".aws/credentials"

ProviderNameSharedCredentials = "SharedCredentialsProvider"
ProviderNameSharedConfig = "SharedConfigCredentials"
ProviderNameSharedConfig = "SharedConfigCredentials"
)

var (
Expand Down
16 changes: 15 additions & 1 deletion test/feature/linux/journald_logs/journald_logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,21 @@ func waitAndStopAgent() {
func TestJournaldUnitsLogs(t *testing.T) {
startAgentAndWaitForInit(t)

// Journal entries for cwagent-unit-test.service are created by Terraform provisioner.
// The journald receiver follows from the end of the journal (start_at: end,
// which the stanza operator maps to `journalctl --lines=0 --follow`), so it
// only captures entries written AFTER it initializes. The Terraform
// provisioner creates and starts cwagent-unit-test.service during instance
// setup, minutes before the agent runs, so those entries predate the
// receiver and are not collected. Re-trigger the unit here so that
// fresh cwagent-unit-test.service entries are produced while the receiver is
// following.
for i := 0; i < 3; i++ {
if err := exec.Command("bash", "-c", "sudo systemctl start cwagent-unit-test.service").Run(); err != nil {
t.Logf("warning: failed to start cwagent-unit-test.service: %v", err)
}
time.Sleep(2 * time.Second)
}

waitAndStopAgent()

instanceId := awsservice.GetInstanceId()
Expand Down
Loading
Loading