From ee5bc81c3990b9951ee49671271d5f346e157835 Mon Sep 17 00:00:00 2001 From: Michael Freeman Date: Wed, 8 Apr 2026 22:42:21 -0500 Subject: [PATCH 1/3] Skip metadata interception in non-root containers --- pkg/sciontool/metadata/server.go | 64 ++++++++++++++++----------- pkg/sciontool/metadata/server_test.go | 19 ++++++++ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/pkg/sciontool/metadata/server.go b/pkg/sciontool/metadata/server.go index 0771f562..05793524 100644 --- a/pkg/sciontool/metadata/server.go +++ b/pkg/sciontool/metadata/server.go @@ -54,6 +54,11 @@ type Config struct { TokenFunc func() string } +const ( + modeBlock = "block" + modeAssign = "assign" +) + // ConfigFromEnv reads metadata server configuration from environment variables. // Returns nil if SCION_METADATA_MODE is not set. func ConfigFromEnv() *Config { @@ -167,36 +172,41 @@ func (s *Server) Start(ctx context.Context) error { // Set up network-level interception for the GCE metadata server IP. // // For block mode: we apply BOTH a REDIRECT (so GCP SDKs hitting the IP - // get a clean HTTP 403 from the sidecar) AND a filter-level REJECT or - // route-level block as defense-in-depth. If the nat REDIRECT is - // ineffective for any reason (wrong iptables backend, missing kernel - // module), the filter/route block ensures the real metadata server is - // unreachable. The REJECT rule is placed after the nat REDIRECT in - // processing order, so when REDIRECT works the REJECT never fires. + // get a clean HTTP 403 from the sidecar) AND a filter-level REJECT as + // defense-in-depth. // // For assign mode: only the REDIRECT is needed. - if err := setupIPTablesRedirect(s.config.Port); err != nil { - // Non-fatal: iptables may not be available (no NET_ADMIN cap, non-Docker runtime). - // The GCE_METADATA_HOST / GCE_METADATA_ROOT env vars are the primary mechanism. - log.Debug("iptables redirect not available: %v", err) - } else { - s.iptablesConfigured = true - } - - if s.config.Mode == "block" { - // Defense-in-depth: block traffic to the metadata IP at the - // filter/route level so that even if the nat REDIRECT fails or - // is bypassed, direct access to the real metadata server is denied. - method, err := setupMetadataBlock() - if err != nil { - log.Error("metadata block: failed to block metadata IP — direct access to %s may still be possible: %v", metadataIP, err) + // + // In non-root containers (notably hosted Kubernetes agents), iptables + // interception is not available. In that case the metadata env vars are the + // primary mechanism and we skip the interception setup entirely to avoid + // misleading warnings. + if shouldAttemptMetadataInterception(os.Getuid()) { + if err := setupIPTablesRedirect(s.config.Port); err != nil { + // Non-fatal: iptables may not be available (no NET_ADMIN cap, non-Docker runtime). + // The GCE_METADATA_HOST / GCE_METADATA_ROOT env vars are the primary mechanism. + log.Debug("iptables redirect not available: %v", err) } else { - s.metadataBlocked = method + s.iptablesConfigured = true } + + if s.config.Mode == modeBlock { + // Defense-in-depth: block traffic to the metadata IP at the + // filter level so that even if the nat REDIRECT fails or + // is bypassed, direct access to the real metadata server is denied. + method, err := setupMetadataBlock() + if err != nil { + log.Error("metadata block: failed to block metadata IP — direct access to %s may still be possible: %v", metadataIP, err) + } else { + s.metadataBlocked = method + } + } + } else { + log.Debug("Skipping metadata IP interception: process is not running as root") } // Start proactive refresh if in assign mode - if s.config.Mode == "assign" { + if s.config.Mode == modeAssign { go s.proactiveRefreshLoop(ctx) } @@ -216,6 +226,10 @@ func (s *Server) Start(ctx context.Context) error { return nil } +func shouldAttemptMetadataInterception(uid int) bool { + return uid == 0 +} + // Stop gracefully shuts down the server. func (s *Server) Stop() { if s.cancel != nil { @@ -279,7 +293,7 @@ func isRecursive(r *http.Request) bool { } func (s *Server) handleServiceAccountList(w http.ResponseWriter, r *http.Request) { - if s.config.Mode == "block" { + if s.config.Mode == modeBlock { http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -327,7 +341,7 @@ func (s *Server) handleServiceAccount(w http.ResponseWriter, r *http.Request, pa return } - if s.config.Mode == "block" { + if s.config.Mode == modeBlock { http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/pkg/sciontool/metadata/server_test.go b/pkg/sciontool/metadata/server_test.go index 55effd28..5f0a08e6 100644 --- a/pkg/sciontool/metadata/server_test.go +++ b/pkg/sciontool/metadata/server_test.go @@ -37,6 +37,25 @@ func freePort(t *testing.T) int { return port } +func TestShouldAttemptMetadataInterception(t *testing.T) { + tests := []struct { + name string + uid int + want bool + }{ + {name: "root", uid: 0, want: true}, + {name: "non-root", uid: 1000, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldAttemptMetadataInterception(tt.uid); got != tt.want { + t.Fatalf("shouldAttemptMetadataInterception(%d) = %v, want %v", tt.uid, got, tt.want) + } + }) + } +} + func TestMetadataServer_HealthCheck(t *testing.T) { port := freePort(t) srv := New(Config{ From d6ec795825e3526c07f6fb6c7f9cf15dd53db66a Mon Sep 17 00:00:00 2001 From: Michael Freeman Date: Fri, 10 Apr 2026 17:20:11 -0500 Subject: [PATCH 2/3] Refactor metadata interception guard clauses --- pkg/sciontool/metadata/server.go | 54 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/pkg/sciontool/metadata/server.go b/pkg/sciontool/metadata/server.go index 05793524..8da2a648 100644 --- a/pkg/sciontool/metadata/server.go +++ b/pkg/sciontool/metadata/server.go @@ -181,29 +181,7 @@ func (s *Server) Start(ctx context.Context) error { // interception is not available. In that case the metadata env vars are the // primary mechanism and we skip the interception setup entirely to avoid // misleading warnings. - if shouldAttemptMetadataInterception(os.Getuid()) { - if err := setupIPTablesRedirect(s.config.Port); err != nil { - // Non-fatal: iptables may not be available (no NET_ADMIN cap, non-Docker runtime). - // The GCE_METADATA_HOST / GCE_METADATA_ROOT env vars are the primary mechanism. - log.Debug("iptables redirect not available: %v", err) - } else { - s.iptablesConfigured = true - } - - if s.config.Mode == modeBlock { - // Defense-in-depth: block traffic to the metadata IP at the - // filter level so that even if the nat REDIRECT fails or - // is bypassed, direct access to the real metadata server is denied. - method, err := setupMetadataBlock() - if err != nil { - log.Error("metadata block: failed to block metadata IP — direct access to %s may still be possible: %v", metadataIP, err) - } else { - s.metadataBlocked = method - } - } - } else { - log.Debug("Skipping metadata IP interception: process is not running as root") - } + s.configureMetadataInterception(os.Getuid()) // Start proactive refresh if in assign mode if s.config.Mode == modeAssign { @@ -230,6 +208,36 @@ func shouldAttemptMetadataInterception(uid int) bool { return uid == 0 } +func (s *Server) configureMetadataInterception(uid int) { + if !shouldAttemptMetadataInterception(uid) { + log.Debug("Skipping metadata IP interception: process is not running as root") + return + } + + if err := setupIPTablesRedirect(s.config.Port); err != nil { + // Non-fatal: iptables may not be available (no NET_ADMIN cap, non-Docker runtime). + // The GCE_METADATA_HOST / GCE_METADATA_ROOT env vars are the primary mechanism. + log.Debug("iptables redirect not available: %v", err) + } else { + s.iptablesConfigured = true + } + + if s.config.Mode != modeBlock { + return + } + + // Defense-in-depth: block traffic to the metadata IP at the filter level + // so that even if the nat REDIRECT fails or is bypassed, direct access to + // the real metadata server is denied. + method, err := setupMetadataBlock() + if err != nil { + log.Error("metadata block: failed to block metadata IP — direct access to %s may still be possible: %v", metadataIP, err) + return + } + + s.metadataBlocked = method +} + // Stop gracefully shuts down the server. func (s *Server) Stop() { if s.cancel != nil { From 2cd7be8e806b5c62da56c25806e154d7d7cc94b1 Mon Sep 17 00:00:00 2001 From: Michael Freeman Date: Fri, 10 Apr 2026 17:25:28 -0500 Subject: [PATCH 3/3] Remove metadata interception success else-branch --- pkg/sciontool/metadata/server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/sciontool/metadata/server.go b/pkg/sciontool/metadata/server.go index 8da2a648..1b984399 100644 --- a/pkg/sciontool/metadata/server.go +++ b/pkg/sciontool/metadata/server.go @@ -214,11 +214,13 @@ func (s *Server) configureMetadataInterception(uid int) { return } - if err := setupIPTablesRedirect(s.config.Port); err != nil { + err := setupIPTablesRedirect(s.config.Port) + if err != nil { // Non-fatal: iptables may not be available (no NET_ADMIN cap, non-Docker runtime). // The GCE_METADATA_HOST / GCE_METADATA_ROOT env vars are the primary mechanism. log.Debug("iptables redirect not available: %v", err) - } else { + } + if err == nil { s.iptablesConfigured = true }