Skip to content
Open
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
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ The AWS Multi-ENI Controller requires specific IAM permissions to function prope
"ec2:DescribeNetworkInterfaces",
"ec2:AttachNetworkInterface",
"ec2:DetachNetworkInterface",
"ec2:DescribeInstances",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups"
"ec2:DescribeSecurityGroups",
"ec2:ModifyInstanceMetadataOptions"
],
"Resource": "*"
}
Expand Down
82 changes: 78 additions & 4 deletions pkg/aws/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,11 @@ func (c *EC2Client) configureIMDSWithFallback(ctx context.Context, hopLimit int3
}

// Strategy 4: Configure all instances in the current VPC (last resort)
if err := c.tryVPCWideConfiguration(ctx, hopLimit); err == nil {
// Resolve VPC ID first to scope the configuration to the correct VPC
vpcID, err := c.resolveCurrentVPCID(ctx)
if err != nil {
c.Logger.Info("Failed to resolve VPC ID, skipping VPC-wide configuration", "error", err.Error())
} else if err := c.tryVPCWideConfiguration(ctx, hopLimit, vpcID); err == nil {
c.Logger.Info("Successfully configured IMDS hop limit using VPC-wide approach")
return nil
}
Expand Down Expand Up @@ -1125,7 +1129,11 @@ func (c *EC2Client) tryPrivateIPBasedConfiguration(ctx context.Context, hopLimit
}

// tryVPCWideConfiguration attempts to configure IMDS for all instances in the VPC (last resort)
func (c *EC2Client) tryVPCWideConfiguration(ctx context.Context, hopLimit int32) error {
func (c *EC2Client) tryVPCWideConfiguration(ctx context.Context, hopLimit int32, vpcID string) error {
if vpcID == "" {
return fmt.Errorf("cannot perform VPC-wide configuration without a VPC ID")
}

// Check if aggressive configuration is enabled
aggressiveConfig := os.Getenv("IMDS_AGGRESSIVE_CONFIGURATION")
if aggressiveConfig != "true" {
Expand All @@ -1136,15 +1144,19 @@ func (c *EC2Client) tryVPCWideConfiguration(ctx context.Context, hopLimit int32)
// This is a last resort strategy - configure IMDS for all instances that might need it
// We'll look for instances that have hop limit 1 and are in running state

c.Logger.Info("Attempting VPC-wide IMDS configuration as last resort")
c.Logger.Info("Attempting VPC-wide IMDS configuration as last resort", "vpcID", vpcID)

// Get all running instances in the region
// Get all running instances in the VPC
input := &ec2.DescribeInstancesInput{
Filters: []types.Filter{
{
Name: aws.String("instance-state-name"),
Values: []string{"running"},
},
{
Name: aws.String("vpc-id"),
Values: []string{vpcID},
},
},
}

Expand Down Expand Up @@ -1201,6 +1213,68 @@ func (c *EC2Client) tryVPCWideConfiguration(ctx context.Context, hopLimit int32)
return nil
}

// resolveCurrentVPCID determines the VPC ID of the current instance by looking up its private IP
func (c *EC2Client) resolveCurrentVPCID(ctx context.Context) (string, error) {
if c.EC2 == nil {
return "", fmt.Errorf("EC2 client is not initialized")
}

privateIP, err := c.getPrivateIPFromNetworkInterface()
if err != nil {
return "", fmt.Errorf("failed to get private IP for VPC resolution: %v", err)
}

if privateIP == "" {
return "", fmt.Errorf("no private IP found for VPC resolution")
}

c.Logger.V(1).Info("Resolving VPC ID using private IP", "privateIP", privateIP)

input := &ec2.DescribeInstancesInput{
Filters: []types.Filter{
{
Name: aws.String("private-ip-address"),
Values: []string{privateIP},
},
{
Name: aws.String("instance-state-name"),
Values: []string{"running", "pending"},
},
},
}

result, err := c.EC2.DescribeInstances(ctx, input)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveCurrentVPCID will panic if c.EC2 is nil (e.g., when EC2Client is constructed without NewEC2Client), since it unconditionally calls c.EC2.DescribeInstances here; consider returning a clear error when the client isn't initialized.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if err != nil {
return "", fmt.Errorf("failed to describe instances for VPC resolution: %v", err)
}

// Collect unique VPC IDs to detect ambiguity from overlapping CIDRs
vpcIDs := make(map[string]struct{})
for _, reservation := range result.Reservations {
for _, instance := range reservation.Instances {
if instance.VpcId != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns the first VpcId from a private-IP lookup; if multiple instances match the same private IP across VPCs (overlapping CIDRs), this could select the wrong VPC and mis-scope the subsequent VPC-wide modifications—consider detecting ambiguity and failing.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

vpcIDs[*instance.VpcId] = struct{}{}
}
}
}

if len(vpcIDs) == 0 {
return "", fmt.Errorf("no VPC ID found for instance with private IP %s", privateIP)
}

if len(vpcIDs) > 1 {
return "", fmt.Errorf("ambiguous VPC resolution: private IP %s matched instances in %d different VPCs", privateIP, len(vpcIDs))
}

// Exactly one VPC matched
for vpcID := range vpcIDs {
c.Logger.V(1).Info("Resolved VPC ID", "vpcID", vpcID, "privateIP", privateIP)
return vpcID, nil
}

return "", fmt.Errorf("no VPC ID found for instance with private IP %s", privateIP)
}

// configureInstanceIMDS configures IMDS hop limit for a specific instance
func (c *EC2Client) configureInstanceIMDS(ctx context.Context, instanceID string, hopLimit int32) error {
c.Logger.Info("Configuring IMDS for instance", "instanceID", instanceID, "hopLimit", hopLimit)
Expand Down
65 changes: 65 additions & 0 deletions pkg/aws/imds_vpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package aws

import (
"context"
"testing"

"github.com/go-logr/logr/testr"
)

// TestTryVPCWideConfiguration_EmptyVPCID verifies that tryVPCWideConfiguration
// rejects an empty VPC ID immediately without making any API calls.
func TestTryVPCWideConfiguration_EmptyVPCID(t *testing.T) {
client := &EC2Client{
Logger: testr.New(t),
}

err := client.tryVPCWideConfiguration(context.Background(), 2, "")
if err == nil {
t.Fatal("expected error when VPC ID is empty, got nil")
}

expected := "cannot perform VPC-wide configuration without a VPC ID"
if err.Error() != expected {
t.Errorf("expected error %q, got %q", expected, err.Error())
}
}

// TestTryVPCWideConfiguration_AggressiveDisabled verifies that tryVPCWideConfiguration
// returns an error when aggressive configuration is disabled.
func TestTryVPCWideConfiguration_AggressiveDisabled(t *testing.T) {
// t.Setenv automatically restores the original value after the test
t.Setenv("IMDS_AGGRESSIVE_CONFIGURATION", "false")

client := &EC2Client{
Logger: testr.New(t),
}

err := client.tryVPCWideConfiguration(context.Background(), 2, "vpc-12345")
if err == nil {
t.Fatal("expected error when aggressive configuration is disabled, got nil")
}

expected := "aggressive configuration disabled"
if err.Error() != expected {
t.Errorf("expected error %q, got %q", expected, err.Error())
}
}

// TestResolveCurrentVPCID_NilEC2Client verifies that resolveCurrentVPCID
// returns a clear error when the EC2 client is not initialized.
func TestResolveCurrentVPCID_NilEC2Client(t *testing.T) {
client := &EC2Client{
Logger: testr.New(t),
}

_, err := client.resolveCurrentVPCID(context.Background())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be flaky (or panic): if getPrivateIPFromNetworkInterface() finds any private IP on the test host, resolveCurrentVPCID() will reach client.EC2.DescribeInstances(...) while EC2 is nil.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if err == nil {
t.Fatal("expected error when EC2 client is nil, got nil")
}

expected := "EC2 client is not initialized"
if err.Error() != expected {
t.Errorf("expected error %q, got %q", expected, err.Error())
}
}
Loading