rootaws: root keeps an inherited proxy but always bypasses it for IMDS (#267)
NO_PROXY and no_proxy gain 169.254.169.254 and fd00:ec2::254 for root, so a proxy can never answer for the instance metadata service, root's EC2 credential source. Off-cloud proxies keep working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+52
−0
internal/rootaws/environment.go+25 −0internal/rootaws/environment_test.go+27 −0
diff --git a/internal/rootaws/environment.go b/internal/rootaws/environment.go| index d4a7055aa..343ea793b 100644 |
| --- a/internal/rootaws/environment.go |
| +++ b/internal/rootaws/environment.go |
| @@ -62,6 +62,12 @@ func scrubEnvironment(euid int) { |
| } |
| _ = os.Unsetenv(key) |
| } |
| + // A proxy may be needed off-cloud, so it stays, but it never answers for |
| + // the instance metadata service: that is where root's EC2 credentials |
| + // come from. |
| + for _, key := range []string{"NO_PROXY", "no_proxy"} { |
| + _ = os.Setenv(key, withIMDSBypass(os.Getenv(key))) |
| + } |
| // Root never reads shared config, not even under its own HOME: a |
| // root-owned HOME can still hold a .aws the service user can write. |
| _ = os.Setenv("AWS_CONFIG_FILE", disabledSharedFile) |
| @@ -70,3 +76,22 @@ func scrubEnvironment(euid int) { |
| log.Print("ALERT: root AWS file, profile, endpoint and trust-store environment overrides ignored") |
| } |
| } |
| + |
| +// imdsHosts are the IPv4 and IPv6 instance metadata addresses. |
| +var imdsHosts = []string{"169.254.169.254", "fd00:ec2::254"} |
| + |
| +func withIMDSBypass(noProxy string) string { |
| + entries := strings.FieldsFunc(noProxy, func(r rune) bool { return r == ',' }) |
| + for _, host := range imdsHosts { |
| + found := false |
| + for _, entry := range entries { |
| + if strings.TrimSpace(entry) == host { |
| + found = true |
| + } |
| + } |
| + if !found { |
| + entries = append(entries, host) |
| + } |
| + } |
| + return strings.Join(entries, ",") |
| +} |
diff --git a/internal/rootaws/environment_test.go b/internal/rootaws/environment_test.go| index 1f56a3d98..3d4dc9774 100644 |
| --- a/internal/rootaws/environment_test.go |
| +++ b/internal/rootaws/environment_test.go |
| @@ -122,3 +122,30 @@ func TestScrubEnvironmentBlocksDependencySharedConfig(t *testing.T) { |
| t.Fatal("disabled path exists") |
| } |
| } |
| + |
| +func TestScrubEnvironmentKeepsProxyButBypassesIMDS(t *testing.T) { |
| + for _, tc := range []struct{ in, want string }{ |
| + {"", "169.254.169.254,fd00:ec2::254"}, |
| + {"example.com", "example.com,169.254.169.254,fd00:ec2::254"}, |
| + {"169.254.169.254,example.com", "169.254.169.254,example.com,fd00:ec2::254"}, |
| + } { |
| + t.Setenv("HTTP_PROXY", "http://proxy.example.com:3128") |
| + t.Setenv("NO_PROXY", tc.in) |
| + t.Setenv("no_proxy", tc.in) |
| + scrubEnvironment(0) |
| + if os.Getenv("HTTP_PROXY") != "http://proxy.example.com:3128" { |
| + t.Fatal("proxy removed") |
| + } |
| + if got := os.Getenv("NO_PROXY"); got != tc.want { |
| + t.Errorf("NO_PROXY(%q) = %q, want %q", tc.in, got, tc.want) |
| + } |
| + if got := os.Getenv("no_proxy"); got != tc.want { |
| + t.Errorf("no_proxy(%q) = %q, want %q", tc.in, got, tc.want) |
| + } |
| + } |
| + t.Setenv("NO_PROXY", "") |
| + scrubEnvironment(1001) |
| + if os.Getenv("NO_PROXY") != "" { |
| + t.Fatal("non-root NO_PROXY changed") |
| + } |
| +} |