From 39c0ffe6d63c46fc51a7e377b47b78148bd26561 Mon Sep 17 00:00:00 2001 From: Akram Date: Tue, 16 Jun 2026 10:11:47 +0200 Subject: [PATCH 1/3] Feat: Add egressEnforcement opt-out for proxy-sidecar mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On managed OpenShift platforms (ROSA HCP), iptables is unavailable in init containers due to missing kernel modules (iptable_nat) and SELinux restrictions. PR #407 made enforce-redirect always-on in proxy-sidecar mode, which breaks pod creation on these platforms. Add an egressEnforcement field (enforce-redirect | none) to the AgentRuntime CR and namespace authbridge-runtime-config ConfigMap, following the same resolution chain as authBridgeMode and mtlsMode: 1. AgentRuntime CR .spec.egressEnforcement (per-workload) 2. Namespace authbridge-runtime-config egressEnforcement (namespace) 3. "enforce-redirect" (cluster default, preserves current behavior) When set to "none", proxy-init is not injected in proxy-sidecar/lite modes. Egress relies on HTTP_PROXY (cooperative) + inbound AuthBridge on destinations + NetworkPolicy. Unknown values fail closed to enforce-redirect. envoy-sidecar mode is unaffected — its proxy-init (redirect mode) is structural and always injected regardless of this setting. Ref: RHAIENG-5702 Ref: https://github.com/kagenti/kagenti-extensions/issues/502 Assisted-By: Claude (Anthropic AI) Signed-off-by: Akram --- .../api/v1alpha1/agentruntime_types.go | 25 +++ .../webhook/injector/agentruntime_config.go | 15 +- .../internal/webhook/injector/constants.go | 13 ++ .../webhook/injector/namespace_config.go | 20 ++ .../internal/webhook/injector/pod_mutator.go | 70 ++++-- .../webhook/injector/pod_mutator_test.go | 201 +++++++++++++++++- 6 files changed, 330 insertions(+), 14 deletions(-) diff --git a/kagenti-operator/api/v1alpha1/agentruntime_types.go b/kagenti-operator/api/v1alpha1/agentruntime_types.go index 50ca0bba..87345acc 100644 --- a/kagenti-operator/api/v1alpha1/agentruntime_types.go +++ b/kagenti-operator/api/v1alpha1/agentruntime_types.go @@ -114,6 +114,31 @@ type AgentRuntimeSpec struct { // +kubebuilder:default=permissive // +kubebuilder:validation:Enum=disabled;permissive;strict MTLSMode string `json:"mtlsMode,omitempty"` + + // EgressEnforcement controls whether the proxy-init init container is + // injected for fail-closed egress capture in proxy-sidecar / lite modes. + // + // Values: + // enforce-redirect (default) — proxy-init is injected with iptables + // rules that transparently REDIRECT egress bypassing + // HTTP_PROXY to AuthBridge's transparent listener. + // Requires NET_ADMIN capability and a kernel that + // supports iptables (legacy or nft). + // none — proxy-init is NOT injected. Egress enforcement + // relies on HTTP_PROXY (cooperative) + inbound + // AuthBridge on destinations + NetworkPolicy. + // Use on platforms where iptables is unavailable + // (e.g. ROSA HCP, managed OpenShift). + // + // Resolution: AgentRuntime CR > namespace authbridge-runtime-config + // egressEnforcement field > "enforce-redirect" (default). + // + // Does not affect envoy-sidecar mode, which always uses proxy-init + // for its structural iptables redirect. + // + // +optional + // +kubebuilder:validation:Enum=enforce-redirect;none + EgressEnforcement string `json:"egressEnforcement,omitempty"` } // IdentitySpec configures workload identity for an AgentRuntime. diff --git a/kagenti-operator/internal/webhook/injector/agentruntime_config.go b/kagenti-operator/internal/webhook/injector/agentruntime_config.go index ee2e7d73..89a440e9 100644 --- a/kagenti-operator/internal/webhook/injector/agentruntime_config.go +++ b/kagenti-operator/internal/webhook/injector/agentruntime_config.go @@ -59,6 +59,12 @@ type AgentRuntimeOverrides struct { // authbridge-runtime-config mtls.mode (if set) or "permissive" // applies. MTLSMode *string + + // Egress enforcement — from .spec.egressEnforcement + // Nil = no per-workload override; the namespace's + // authbridge-runtime-config egressEnforcement (if set) or + // "enforce-redirect" (default) applies. + EgressEnforcement *string } // ReadAgentRuntimeOverrides reads the AgentRuntime CR for a given workload @@ -125,11 +131,18 @@ func extractOverrides(rt *agentv1alpha1.AgentRuntime) *AgentRuntimeOverrides { overrides.MTLSMode = &mode } + // .spec.egressEnforcement + if rt.Spec.EgressEnforcement != "" { + ee := rt.Spec.EgressEnforcement + overrides.EgressEnforcement = &ee + } + arConfigLog.Info("AgentRuntime overrides extracted", "hasSpiffeTrustDomain", overrides.SpiffeTrustDomain != nil, "hasClientRegistration", overrides.ClientRegistrationProvider != nil, "hasAuthBridgeMode", overrides.AuthBridgeMode != nil, - "hasMTLSMode", overrides.MTLSMode != nil) + "hasMTLSMode", overrides.MTLSMode != nil, + "hasEgressEnforcement", overrides.EgressEnforcement != nil) return overrides } diff --git a/kagenti-operator/internal/webhook/injector/constants.go b/kagenti-operator/internal/webhook/injector/constants.go index b740a091..dde5bc6d 100644 --- a/kagenti-operator/internal/webhook/injector/constants.go +++ b/kagenti-operator/internal/webhook/injector/constants.go @@ -57,6 +57,19 @@ const ( ProxyInitModeEnforceRedirect ProxyInitMode = "enforce-redirect" ) +// Egress enforcement modes for proxy-sidecar / lite paths. +// Controls whether proxy-init is injected for fail-closed egress capture. +const ( + // EgressEnforcementEnforceRedirect injects proxy-init with iptables + // rules (default). Requires NET_ADMIN and a kernel with iptables support. + EgressEnforcementEnforceRedirect = "enforce-redirect" + + // EgressEnforcementNone skips proxy-init injection. Egress relies on + // HTTP_PROXY (cooperative) + inbound AuthBridge + NetworkPolicy. + // Use on platforms where iptables is unavailable (ROSA HCP, managed OpenShift). + EgressEnforcementNone = "none" +) + // mTLS modes for the proxy-sidecar / lite paths. Selected per workload // via AgentRuntime CR `Spec.MTLSMode`, falling back to the namespace // `authbridge-runtime-config` ConfigMap's `mtls.mode` field, then diff --git a/kagenti-operator/internal/webhook/injector/namespace_config.go b/kagenti-operator/internal/webhook/injector/namespace_config.go index 1009955a..e3ebf539 100644 --- a/kagenti-operator/internal/webhook/injector/namespace_config.go +++ b/kagenti-operator/internal/webhook/injector/namespace_config.go @@ -160,6 +160,26 @@ func ExtractMode(authbridgeYAML string) string { return top.Mode } +// ExtractEgressEnforcement parses an authbridge-runtime-config config.yaml +// string and returns the value of its top-level `egressEnforcement:` key. +// Returns "" if the YAML is empty, malformed, or has no `egressEnforcement` +// field — in any of those cases the caller should fall back to the next +// resolution layer (or the "enforce-redirect" default). +func ExtractEgressEnforcement(authbridgeYAML string) string { + if authbridgeYAML == "" { + return "" + } + var top struct { + EgressEnforcement string `json:"egressEnforcement"` + } + if err := yaml.Unmarshal([]byte(authbridgeYAML), &top); err != nil { + nsConfigLog.Info("WARN: failed to parse authbridge-runtime-config config.yaml for egressEnforcement; falling back to next resolution layer", + "error", err.Error()) + return "" + } + return top.EgressEnforcement +} + // ExtractMTLSMode parses an authbridge-runtime-config config.yaml string // and returns the value of its `mtls.mode` field. Returns "" if the YAML // is empty, malformed, has no `mtls` block, or its `mode` field is unset diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator.go b/kagenti-operator/internal/webhook/injector/pod_mutator.go index 3fb1754c..f048df2a 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator.go @@ -301,6 +301,44 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp } } + // ======================================== + // Resolve egressEnforcement (CR > namespace > enforce-redirect) + // ======================================== + // + // Controls proxy-init injection in proxy-sidecar / lite modes. + // "enforce-redirect" (default): proxy-init injected with iptables. + // "none": proxy-init skipped — cooperative HTTP_PROXY only. + // envoy-sidecar mode ignores this (proxy-init is structural there). + egressEnforcement := "" + egressEnforcementSource := "" + if arOverrides != nil && arOverrides.EgressEnforcement != nil { + egressEnforcement = *arOverrides.EgressEnforcement + egressEnforcementSource = "agentruntime-cr" + } + if egressEnforcement == "" { + if ee := ExtractEgressEnforcement(nsConfig.AuthBridgeRuntimeYAML); ee != "" { + egressEnforcement = ee + egressEnforcementSource = "namespace-configmap" + } + } + if egressEnforcement == "" { + egressEnforcement = EgressEnforcementEnforceRedirect + egressEnforcementSource = "cluster-default" + } + switch egressEnforcement { + case EgressEnforcementEnforceRedirect, EgressEnforcementNone: + // recognized, keep as-is + default: + mutatorLog.Info("WARN: unrecognized egressEnforcement; defaulting to enforce-redirect (fail closed)", + "namespace", namespace, "crName", crName, + "unrecognized", egressEnforcement, "source", egressEnforcementSource) + egressEnforcement = EgressEnforcementEnforceRedirect + egressEnforcementSource = "default-invalid-fallback" + } + mutatorLog.Info("resolved egress enforcement", + "namespace", namespace, "crName", crName, + "mode", egressEnforcement, "source", egressEnforcementSource) + // ======================================== // Resolve AllowedAudiences (from AgentRuntime CR) // ======================================== @@ -534,19 +572,27 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp injectHTTPProxyEnv(c, forwardProxyPort) } - // Fail-closed egress enforcement (always-on for proxy-sidecar / lite). + // Egress enforcement for proxy-sidecar / lite. // HTTP_PROXY above is cooperative — an app that ignores it egresses - // directly. The enforce-redirect proxy-init guard transparently REDIRECTs - // any bypass egress to the forward proxy's transparent listener, so it is - // captured rather than dropped and nothing breaks. envoy-sidecar enforces - // structurally via its own transparent redirect, so this is the - // proxy-sidecar / lite path only. The exempted PROXY_UID equals the proxy - // container's RunAsUser (both b.cfg.Proxy.UID). - if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { - podSpec.InitContainers = append(podSpec.InitContainers, - builder.BuildProxyInitContainer(ProxyInitModeEnforceRedirect, "", "")) - mutatorLog.Info("proxy-sidecar egress enforcement enabled (enforce-redirect)", - "namespace", namespace, "crName", crName) + // directly. When egressEnforcement is "enforce-redirect" (default), + // proxy-init is injected with iptables rules that transparently + // REDIRECT bypass egress to AuthBridge's transparent listener. + // When "none", proxy-init is skipped — use on platforms where + // iptables is unavailable (ROSA HCP, managed OpenShift). + // envoy-sidecar mode has its own structural proxy-init and ignores + // this setting. + if egressEnforcement == EgressEnforcementEnforceRedirect { + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + podSpec.InitContainers = append(podSpec.InitContainers, + builder.BuildProxyInitContainer(ProxyInitModeEnforceRedirect, "", "")) + mutatorLog.Info("proxy-sidecar egress enforcement enabled (enforce-redirect)", + "namespace", namespace, "crName", crName) + } + } else { + mutatorLog.Info("proxy-sidecar egress enforcement disabled (cooperative mode)", + "namespace", namespace, "crName", crName, + "egressEnforcement", egressEnforcement, + "source", egressEnforcementSource) } // spiffe-helper is bundled in the authbridge combined image and diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go index b113ce58..111a4e1f 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go @@ -176,7 +176,7 @@ func TestInjectAuthBridge_NoAgentRuntime_InjectsWithDefaults(t *testing.T) { t.Errorf("unexpected %s container in proxy-sidecar mode", EnvoyProxyContainerName) } if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { - t.Errorf("expected %s init container in proxy-sidecar mode (always-on enforce-redirect)", ProxyInitContainerName) + t.Errorf("expected %s init container in proxy-sidecar mode (default enforce-redirect)", ProxyInitContainerName) } } @@ -1915,3 +1915,202 @@ pipeline: t.Errorf("allowed_audiences = %v, want [agent-aud] (AgentRuntime CR must override base YAML)", audList) } } + +// ======================================== +// EgressEnforcement tests +// ======================================== + +// newAgentRuntimeWithEgressEnforcement creates an AgentRuntime CR with +// proxy-sidecar mode and the given egressEnforcement value. +func newAgentRuntimeWithEgressEnforcement(namespace, targetName, ee string) *agentv1alpha1.AgentRuntime { + rt := newAgentRuntimeWithMode(namespace, targetName, ModeProxySidecar) + rt.Spec.EgressEnforcement = ee + return rt +} + +func TestInjectAuthBridge_EgressEnforcement_DefaultInjectsProxyInit(t *testing.T) { + // Default (no egressEnforcement set) → proxy-init should be injected. + m := newTestMutator(newAgentRuntimeWithMode("test-ns", "my-agent", ModeProxySidecar)) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("expected proxy-init when egressEnforcement is unset (default enforce-redirect)") + } +} + +func TestInjectAuthBridge_EgressEnforcement_NoneSkipsProxyInit(t *testing.T) { + // egressEnforcement: none → proxy-init should NOT be injected. + rt := newAgentRuntimeWithEgressEnforcement("test-ns", "my-agent", EgressEnforcementNone) + m := newTestMutator(rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("proxy-init should NOT be injected when egressEnforcement=none") + } + // authbridge-proxy sidecar should still be injected + if !containerExists(podSpec.Containers, AuthBridgeProxyContainerName) { + t.Error("authbridge-proxy should still be injected when egressEnforcement=none") + } +} + +func TestInjectAuthBridge_EgressEnforcement_EnforceRedirectInjectsProxyInit(t *testing.T) { + // egressEnforcement: enforce-redirect → proxy-init should be injected. + rt := newAgentRuntimeWithEgressEnforcement("test-ns", "my-agent", EgressEnforcementEnforceRedirect) + m := newTestMutator(rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("expected proxy-init when egressEnforcement=enforce-redirect") + } +} + +func TestInjectAuthBridge_EgressEnforcement_NamespaceConfigMapNone(t *testing.T) { + // Namespace ConfigMap sets egressEnforcement: none, no CR override. + runtimeCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AuthBridgeRuntimeConfigMapName, + Namespace: "test-ns", + }, + Data: map[string]string{ + "config.yaml": "mode: proxy-sidecar\negressEnforcement: none\n", + }, + } + rt := newAgentRuntimeWithMode("test-ns", "my-agent", ModeProxySidecar) + rt.Spec.EgressEnforcement = "" // no CR override + m := newTestMutator(rt, runtimeCM) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("proxy-init should NOT be injected when namespace ConfigMap sets egressEnforcement=none") + } +} + +func TestInjectAuthBridge_EgressEnforcement_CROverridesNamespace(t *testing.T) { + // Namespace ConfigMap says enforce-redirect, but CR says none → CR wins. + runtimeCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AuthBridgeRuntimeConfigMapName, + Namespace: "test-ns", + }, + Data: map[string]string{ + "config.yaml": "mode: proxy-sidecar\negressEnforcement: enforce-redirect\n", + }, + } + rt := newAgentRuntimeWithEgressEnforcement("test-ns", "my-agent", EgressEnforcementNone) + m := newTestMutator(rt, runtimeCM) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("CR egressEnforcement=none should override namespace ConfigMap enforce-redirect") + } +} + +func TestInjectAuthBridge_EgressEnforcement_UnknownValueFailsClosed(t *testing.T) { + // Unknown value → fail closed to enforce-redirect (proxy-init injected). + runtimeCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AuthBridgeRuntimeConfigMapName, + Namespace: "test-ns", + }, + Data: map[string]string{ + "config.yaml": "mode: proxy-sidecar\negressEnforcement: typo-value\n", + }, + } + rt := newAgentRuntimeWithMode("test-ns", "my-agent", ModeProxySidecar) + m := newTestMutator(rt, runtimeCM) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("unknown egressEnforcement value should fail closed (inject proxy-init)") + } +} + +func TestInjectAuthBridge_EgressEnforcement_EnvoySidecarIgnoresNone(t *testing.T) { + // In envoy-sidecar mode, egressEnforcement=none should be ignored — + // proxy-init is structural for envoy-sidecar (redirect mode, not enforce-redirect). + rt := newAgentRuntimeWithMode("test-ns", "my-agent", ModeEnvoySidecar) + rt.Spec.EgressEnforcement = EgressEnforcementNone + m := newTestMutator(rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("envoy-sidecar mode should inject proxy-init regardless of egressEnforcement=none") + } +} From cccd982318cfa7c4c5aea157e724f78bc83c0639 Mon Sep 17 00:00:00 2001 From: Akram Date: Tue, 16 Jun 2026 10:38:56 +0200 Subject: [PATCH 2/3] Feat: Add platform-level allowedEgressEnforcement policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds proxy.allowedEgressEnforcement to the platform config (Helm values → kagenti-platform-config ConfigMap). The webhook validates the resolved egressEnforcement value against this list and overrides it to the first allowed value when not permitted. This gives platform admins governance over egress enforcement modes: - ["enforce-redirect"] → no opt-out allowed - ["none"] → iptables disabled cluster-wide (ROSA HCP) - ["enforce-redirect", "none"] → workloads choose (default) Default allows both modes for backward compatibility. Ref: RHAIENG-5702 Assisted-By: Claude (Anthropic AI) Signed-off-by: Akram --- .../internal/webhook/config/defaults.go | 4 + .../internal/webhook/config/types.go | 21 +++++ .../internal/webhook/injector/pod_mutator.go | 22 +++++ .../webhook/injector/pod_mutator_test.go | 86 +++++++++++++++++++ 4 files changed, 133 insertions(+) diff --git a/kagenti-operator/internal/webhook/config/defaults.go b/kagenti-operator/internal/webhook/config/defaults.go index 86f6c1e7..58fb5abf 100644 --- a/kagenti-operator/internal/webhook/config/defaults.go +++ b/kagenti-operator/internal/webhook/config/defaults.go @@ -38,6 +38,10 @@ func CompiledDefaults() *PlatformConfig { // Empty by default: proxy-init auto-detects the iptables backend from // /proc/modules. Set (e.g. "iptables") to force a backend per-platform. IptablesCmd: "", + // Both modes allowed by default. Set to ["none"] on platforms + // where iptables is unavailable (ROSA HCP, managed OpenShift), + // or ["enforce-redirect"] to prevent opt-out. + AllowedEgressEnforcement: []string{"enforce-redirect", "none"}, }, Resources: ResourcesConfig{ EnvoyProxy: corev1.ResourceRequirements{ diff --git a/kagenti-operator/internal/webhook/config/types.go b/kagenti-operator/internal/webhook/config/types.go index 7f2df7fe..b74fe5a8 100644 --- a/kagenti-operator/internal/webhook/config/types.go +++ b/kagenti-operator/internal/webhook/config/types.go @@ -59,6 +59,14 @@ type ProxyConfig struct { // ROSA). Set to "iptables" (nft) or "iptables-legacy" to force a backend // where auto-detection is wrong or undesired. IptablesCmd string `json:"iptablesCmd" yaml:"iptablesCmd"` + + // AllowedEgressEnforcement restricts which egressEnforcement values + // workloads (AgentRuntime CR / namespace ConfigMap) may select. + // The webhook rejects resolved values not in this list, falling back + // to the first entry. Default: ["enforce-redirect", "none"] (both + // allowed). Set to ["enforce-redirect"] to prevent opt-out, or + // ["none"] on platforms where iptables is unavailable. + AllowedEgressEnforcement []string `json:"allowedEgressEnforcement,omitempty" yaml:"allowedEgressEnforcement,omitempty"` } type ResourcesConfig struct { @@ -95,6 +103,11 @@ func (c *PlatformConfig) DeepCopy() *PlatformConfig { copy(result.TokenExchange.DefaultScopes, c.TokenExchange.DefaultScopes) } + if c.Proxy.AllowedEgressEnforcement != nil { + result.Proxy.AllowedEgressEnforcement = make([]string, len(c.Proxy.AllowedEgressEnforcement)) + copy(result.Proxy.AllowedEgressEnforcement, c.Proxy.AllowedEgressEnforcement) + } + // Deep copy ResourceRequirements — ResourceList is a map that would be shared result.Resources.EnvoyProxy = deepCopyResourceRequirements(c.Resources.EnvoyProxy) result.Resources.ProxyInit = deepCopyResourceRequirements(c.Resources.ProxyInit) @@ -148,6 +161,14 @@ func (c *PlatformConfig) Validate() error { default: return fmt.Errorf("proxy.iptablesCmd %q is not a recognized backend (want one of: \"\" (auto-detect), iptables, iptables-nft, iptables-legacy)", c.Proxy.IptablesCmd) } + if len(c.Proxy.AllowedEgressEnforcement) == 0 { + return fmt.Errorf("proxy.allowedEgressEnforcement must not be empty (set [\"enforce-redirect\"] to require enforcement, [\"none\"] to disable it, or both to allow workload choice)") + } + for _, mode := range c.Proxy.AllowedEgressEnforcement { + if mode != "enforce-redirect" && mode != "none" { + return fmt.Errorf("proxy.allowedEgressEnforcement contains invalid value %q (allowed: enforce-redirect, none)", mode) + } + } if c.Images.EnvoyProxy == "" { return fmt.Errorf("images.envoyProxy is required") } diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator.go b/kagenti-operator/internal/webhook/injector/pod_mutator.go index f048df2a..28c23221 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator.go @@ -335,6 +335,19 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp egressEnforcement = EgressEnforcementEnforceRedirect egressEnforcementSource = "default-invalid-fallback" } + // Validate against the platform's allowed list. If the resolved value + // is not permitted, fall back to the first allowed value (fail closed + // when only enforce-redirect is allowed, fail open when only none is + // allowed — the admin controls the list). + allowed := currentConfig.Proxy.AllowedEgressEnforcement + if len(allowed) > 0 && !stringInSlice(egressEnforcement, allowed) { + mutatorLog.Info("WARN: egressEnforcement value not in platform allowedEgressEnforcement; overriding", + "namespace", namespace, "crName", crName, + "requested", egressEnforcement, "allowed", allowed, + "overrideTo", allowed[0]) + egressEnforcement = allowed[0] + egressEnforcementSource = "platform-policy-override" + } mutatorLog.Info("resolved egress enforcement", "namespace", namespace, "crName", crName, "mode", egressEnforcement, "source", egressEnforcementSource) @@ -1112,6 +1125,15 @@ func volumeExists(volumes []corev1.Volume, name string) bool { return false } +func stringInSlice(s string, list []string) bool { + for _, v := range list { + if v == s { + return true + } + } + return false +} + // ensureFSGroup sets fsGroup in the pod security context to enable shared volume access. // This allows containers with different UIDs (spiffe-helper, client-registration, envoy-proxy) // to read/write files in shared volumes like svid-output. diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go index 111a4e1f..7664d9c7 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator_test.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator_test.go @@ -2114,3 +2114,89 @@ func TestInjectAuthBridge_EgressEnforcement_EnvoySidecarIgnoresNone(t *testing.T t.Error("envoy-sidecar mode should inject proxy-init regardless of egressEnforcement=none") } } + +// newTestMutatorWithAllowedEgress creates a test mutator with a custom +// allowedEgressEnforcement platform policy. +func newTestMutatorWithAllowedEgress(allowed []string, objs ...client.Object) *PodMutator { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = appsv1.AddToScheme(scheme) + _ = agentv1alpha1.AddToScheme(scheme) + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build() + return &PodMutator{ + Client: fakeClient, + APIReader: fakeClient, + GetPlatformConfig: func() *config.PlatformConfig { + cfg := config.CompiledDefaults() + cfg.Proxy.AllowedEgressEnforcement = allowed + return cfg + }, + GetFeatureGates: config.DefaultFeatureGates, + } +} + +func TestInjectAuthBridge_EgressEnforcement_PlatformPolicyBlocksNone(t *testing.T) { + // Platform only allows enforce-redirect. CR requests none → overridden. + rt := newAgentRuntimeWithEgressEnforcement("test-ns", "my-agent", EgressEnforcementNone) + m := newTestMutatorWithAllowedEgress([]string{EgressEnforcementEnforceRedirect}, rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("platform policy allows only enforce-redirect; proxy-init should be injected despite CR requesting none") + } +} + +func TestInjectAuthBridge_EgressEnforcement_PlatformPolicyAllowsNone(t *testing.T) { + // Platform allows both modes. CR requests none → honored. + rt := newAgentRuntimeWithEgressEnforcement("test-ns", "my-agent", EgressEnforcementNone) + m := newTestMutatorWithAllowedEgress([]string{EgressEnforcementEnforceRedirect, EgressEnforcementNone}, rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("platform allows none; CR requests none; proxy-init should NOT be injected") + } +} + +func TestInjectAuthBridge_EgressEnforcement_PlatformPolicyOnlyNone(t *testing.T) { + // Platform only allows none (ROSA HCP). Default enforce-redirect → overridden to none. + rt := newAgentRuntimeWithMode("test-ns", "my-agent", ModeProxySidecar) + m := newTestMutatorWithAllowedEgress([]string{EgressEnforcementNone}, rt) + ctx := context.Background() + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent", Image: "my-agent:latest"}, + }, + } + labels := map[string]string{KagentiTypeLabel: KagentiTypeAgent} + + _, err := m.InjectAuthBridge(ctx, podSpec, "test-ns", "my-agent", labels, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if containerExists(podSpec.InitContainers, ProxyInitContainerName) { + t.Error("platform only allows none; proxy-init should NOT be injected even with default enforce-redirect") + } +} From ecea1782d253d9862d3a4aad4af7eda0c559ee64 Mon Sep 17 00:00:00 2001 From: Akram Date: Tue, 16 Jun 2026 15:22:45 +0200 Subject: [PATCH 3/3] Fix: Address review comments on egressEnforcement PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regenerate CRD manifests (make manifests) — egressEnforcement field now appears in agent.kagenti.dev_agentruntimes.yaml with enum validation - Replace stringInSlice helper with slices.Contains (stdlib) - Reject empty allowedEgressEnforcement in Validate() — empty list was a footgun (bypass governance); must be explicitly set - Add allowedEgressEnforcement to charts/kagenti-operator/values.yaml with doc comment for discoverability Ref: RHAIENG-5702 Assisted-By: Claude (Anthropic AI) Signed-off-by: Akram --- .../crds/agent.kagenti.dev_agentruntimes.yaml | 26 ++ ...ent.kagenti.dev_authorizationpolicies.yaml | 240 +++++++++++------- charts/kagenti-operator/values.yaml | 10 + .../agent.kagenti.dev_agentruntimes.yaml | 26 ++ ...ent.kagenti.dev_authorizationpolicies.yaml | 240 +++++++++++------- .../internal/webhook/injector/pod_mutator.go | 11 +- 6 files changed, 351 insertions(+), 202 deletions(-) diff --git a/charts/kagenti-operator/crds/agent.kagenti.dev_agentruntimes.yaml b/charts/kagenti-operator/crds/agent.kagenti.dev_agentruntimes.yaml index 60f96d49..182f6407 100644 --- a/charts/kagenti-operator/crds/agent.kagenti.dev_agentruntimes.yaml +++ b/charts/kagenti-operator/crds/agent.kagenti.dev_agentruntimes.yaml @@ -94,6 +94,32 @@ spec: - lite - waypoint type: string + egressEnforcement: + description: |- + EgressEnforcement controls whether the proxy-init init container is + injected for fail-closed egress capture in proxy-sidecar / lite modes. + + Values: + enforce-redirect (default) — proxy-init is injected with iptables + rules that transparently REDIRECT egress bypassing + HTTP_PROXY to AuthBridge's transparent listener. + Requires NET_ADMIN capability and a kernel that + supports iptables (legacy or nft). + none — proxy-init is NOT injected. Egress enforcement + relies on HTTP_PROXY (cooperative) + inbound + AuthBridge on destinations + NetworkPolicy. + Use on platforms where iptables is unavailable + (e.g. ROSA HCP, managed OpenShift). + + Resolution: AgentRuntime CR > namespace authbridge-runtime-config + egressEnforcement field > "enforce-redirect" (default). + + Does not affect envoy-sidecar mode, which always uses proxy-init + for its structural iptables redirect. + enum: + - enforce-redirect + - none + type: string identity: description: Identity specifies optional per-workload identity overrides properties: diff --git a/charts/kagenti-operator/crds/agent.kagenti.dev_authorizationpolicies.yaml b/charts/kagenti-operator/crds/agent.kagenti.dev_authorizationpolicies.yaml index ff0d240a..ec1ab44a 100644 --- a/charts/kagenti-operator/crds/agent.kagenti.dev_authorizationpolicies.yaml +++ b/charts/kagenti-operator/crds/agent.kagenti.dev_authorizationpolicies.yaml @@ -1,6 +1,9 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 name: authorizationpolicies.agent.kagenti.dev spec: group: agent.kagenti.dev @@ -8,102 +11,147 @@ spec: kind: AuthorizationPolicy listKind: AuthorizationPolicyList plural: authorizationpolicies - singular: authorizationpolicy shortNames: - - ap + - ap + singular: authorizationpolicy scope: Namespaced versions: - - name: v1alpha1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Scope - type: string - jsonPath: .spec.scope - - name: ClientID - type: string - jsonPath: .spec.clientID - - name: Hash - type: string - jsonPath: .status.bundleHash - priority: 1 - - name: Age - type: date - jsonPath: .metadata.creationTimestamp - schema: - openAPIV3Schema: - type: object - required: - - spec - properties: - spec: - type: object - required: - - scope - - policies - properties: - scope: - type: string - enum: - - global - - namespace - - client - default: client - clientID: - type: string - maxLength: 253 - pattern: "^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$" - policies: - type: array - minItems: 1 - items: - type: object - required: - - path - - content - properties: - path: - type: string - minLength: 1 - pattern: "^[a-z0-9][a-z0-9/_.-]*\\.rego$" - content: - type: string - minLength: 1 - x-kubernetes-validations: - - rule: "self.scope == 'client' ? self.clientID != '' : true" - message: "clientID is required when scope is 'client'" - - rule: "self.scope != 'client' ? !has(self.clientID) || self.clientID == '' : true" - message: "clientID must not be set when scope is 'global' or 'namespace'" - status: - type: object - properties: - bundleHash: - type: string - lastBuilt: - type: string - format: date-time - conditions: - type: array - items: - type: object - required: - - type - - status - properties: - type: - type: string - status: - type: string - enum: - - "True" - - "False" - - "Unknown" - lastTransitionTime: - type: string - format: date-time - reason: - type: string - message: - type: string + - additionalPrinterColumns: + - jsonPath: .spec.scope + name: Scope + type: string + - jsonPath: .spec.clientID + name: ClientID + type: string + - jsonPath: .status.bundleHash + name: Hash + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + clientID: + maxLength: 253 + pattern: ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ + type: string + policies: + items: + properties: + content: + minLength: 1 + type: string + path: + minLength: 1 + pattern: ^[a-z0-9][a-z0-9/_.-]*\.rego$ + type: string + required: + - content + - path + type: object + minItems: 1 + type: array + scope: + default: client + enum: + - global + - namespace + - client + type: string + required: + - policies + - scope + type: object + status: + properties: + bundleHash: + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastBuilt: + format: date-time + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index 466148bc..ff137db7 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -234,6 +234,16 @@ defaults: # (nft) or "iptables-legacy" to force a backend where detection is undesired. iptablesCmd: "" + # Platform-level governance: which egressEnforcement modes workloads may select. + # The webhook overrides the resolved value to the first entry when the + # workload's choice is not in this list. + # ["enforce-redirect"] — no opt-out allowed (iptables required) + # ["none"] — iptables disabled cluster-wide (ROSA HCP) + # ["enforce-redirect", "none"] — workloads choose (default) + allowedEgressEnforcement: + - enforce-redirect + - none + # Resource defaults (conservative for dev) # Note: requests must be <= limits resources: diff --git a/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentruntimes.yaml b/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentruntimes.yaml index 60f96d49..182f6407 100644 --- a/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentruntimes.yaml +++ b/kagenti-operator/config/crd/bases/agent.kagenti.dev_agentruntimes.yaml @@ -94,6 +94,32 @@ spec: - lite - waypoint type: string + egressEnforcement: + description: |- + EgressEnforcement controls whether the proxy-init init container is + injected for fail-closed egress capture in proxy-sidecar / lite modes. + + Values: + enforce-redirect (default) — proxy-init is injected with iptables + rules that transparently REDIRECT egress bypassing + HTTP_PROXY to AuthBridge's transparent listener. + Requires NET_ADMIN capability and a kernel that + supports iptables (legacy or nft). + none — proxy-init is NOT injected. Egress enforcement + relies on HTTP_PROXY (cooperative) + inbound + AuthBridge on destinations + NetworkPolicy. + Use on platforms where iptables is unavailable + (e.g. ROSA HCP, managed OpenShift). + + Resolution: AgentRuntime CR > namespace authbridge-runtime-config + egressEnforcement field > "enforce-redirect" (default). + + Does not affect envoy-sidecar mode, which always uses proxy-init + for its structural iptables redirect. + enum: + - enforce-redirect + - none + type: string identity: description: Identity specifies optional per-workload identity overrides properties: diff --git a/kagenti-operator/config/crd/bases/agent.kagenti.dev_authorizationpolicies.yaml b/kagenti-operator/config/crd/bases/agent.kagenti.dev_authorizationpolicies.yaml index ff0d240a..ec1ab44a 100644 --- a/kagenti-operator/config/crd/bases/agent.kagenti.dev_authorizationpolicies.yaml +++ b/kagenti-operator/config/crd/bases/agent.kagenti.dev_authorizationpolicies.yaml @@ -1,6 +1,9 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 name: authorizationpolicies.agent.kagenti.dev spec: group: agent.kagenti.dev @@ -8,102 +11,147 @@ spec: kind: AuthorizationPolicy listKind: AuthorizationPolicyList plural: authorizationpolicies - singular: authorizationpolicy shortNames: - - ap + - ap + singular: authorizationpolicy scope: Namespaced versions: - - name: v1alpha1 - served: true - storage: true - subresources: - status: {} - additionalPrinterColumns: - - name: Scope - type: string - jsonPath: .spec.scope - - name: ClientID - type: string - jsonPath: .spec.clientID - - name: Hash - type: string - jsonPath: .status.bundleHash - priority: 1 - - name: Age - type: date - jsonPath: .metadata.creationTimestamp - schema: - openAPIV3Schema: - type: object - required: - - spec - properties: - spec: - type: object - required: - - scope - - policies - properties: - scope: - type: string - enum: - - global - - namespace - - client - default: client - clientID: - type: string - maxLength: 253 - pattern: "^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$" - policies: - type: array - minItems: 1 - items: - type: object - required: - - path - - content - properties: - path: - type: string - minLength: 1 - pattern: "^[a-z0-9][a-z0-9/_.-]*\\.rego$" - content: - type: string - minLength: 1 - x-kubernetes-validations: - - rule: "self.scope == 'client' ? self.clientID != '' : true" - message: "clientID is required when scope is 'client'" - - rule: "self.scope != 'client' ? !has(self.clientID) || self.clientID == '' : true" - message: "clientID must not be set when scope is 'global' or 'namespace'" - status: - type: object - properties: - bundleHash: - type: string - lastBuilt: - type: string - format: date-time - conditions: - type: array - items: - type: object - required: - - type - - status - properties: - type: - type: string - status: - type: string - enum: - - "True" - - "False" - - "Unknown" - lastTransitionTime: - type: string - format: date-time - reason: - type: string - message: - type: string + - additionalPrinterColumns: + - jsonPath: .spec.scope + name: Scope + type: string + - jsonPath: .spec.clientID + name: ClientID + type: string + - jsonPath: .status.bundleHash + name: Hash + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + clientID: + maxLength: 253 + pattern: ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ + type: string + policies: + items: + properties: + content: + minLength: 1 + type: string + path: + minLength: 1 + pattern: ^[a-z0-9][a-z0-9/_.-]*\.rego$ + type: string + required: + - content + - path + type: object + minItems: 1 + type: array + scope: + default: client + enum: + - global + - namespace + - client + type: string + required: + - policies + - scope + type: object + status: + properties: + bundleHash: + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastBuilt: + format: date-time + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/kagenti-operator/internal/webhook/injector/pod_mutator.go b/kagenti-operator/internal/webhook/injector/pod_mutator.go index 28c23221..ce00e399 100644 --- a/kagenti-operator/internal/webhook/injector/pod_mutator.go +++ b/kagenti-operator/internal/webhook/injector/pod_mutator.go @@ -340,7 +340,7 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp // when only enforce-redirect is allowed, fail open when only none is // allowed — the admin controls the list). allowed := currentConfig.Proxy.AllowedEgressEnforcement - if len(allowed) > 0 && !stringInSlice(egressEnforcement, allowed) { + if !slices.Contains(allowed, egressEnforcement) { mutatorLog.Info("WARN: egressEnforcement value not in platform allowedEgressEnforcement; overriding", "namespace", namespace, "crName", crName, "requested", egressEnforcement, "allowed", allowed, @@ -1125,15 +1125,6 @@ func volumeExists(volumes []corev1.Volume, name string) bool { return false } -func stringInSlice(s string, list []string) bool { - for _, v := range list { - if v == s { - return true - } - } - return false -} - // ensureFSGroup sets fsGroup in the pod security context to enable shared volume access. // This allows containers with different UIDs (spiffe-helper, client-registration, envoy-proxy) // to read/write files in shared volumes like svid-output.