diff --git a/pkg/sciontool/metadata/server.go b/pkg/sciontool/metadata/server.go index 0771f562..1b984399 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,19 @@ 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) - } else { - s.metadataBlocked = method - } - } + // + // 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. + s.configureMetadataInterception(os.Getuid()) // Start proactive refresh if in assign mode - if s.config.Mode == "assign" { + if s.config.Mode == modeAssign { go s.proactiveRefreshLoop(ctx) } @@ -216,6 +204,42 @@ func (s *Server) Start(ctx context.Context) error { return nil } +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 + } + + 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) + } + if err == nil { + 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 { @@ -279,7 +303,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 +351,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{