diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index 411e5c5e..466148bc 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -228,6 +228,11 @@ defaults: # Cluster DNS is kept direct by proxy-init itself (it reads the pod's # /etc/resolv.conf nameservers), so there is no in-cluster CIDR knob to set — # works on Kind / OpenShift / EKS / NodeLocal-DNSCache with no per-cluster config. + # iptables backend override, injected as IPTABLES_CMD. Empty (default) lets + # proxy-init auto-detect from /proc/modules (iptable_nat loaded => legacy, as + # on Kind/kubeadm; absent => nft, as on OpenShift/ROSA). Set to "iptables" + # (nft) or "iptables-legacy" to force a backend where detection is undesired. + iptablesCmd: "" # Resource defaults (conservative for dev) # Note: requests must be <= limits diff --git a/kagenti-operator/internal/webhook/config/defaults.go b/kagenti-operator/internal/webhook/config/defaults.go index 50aa7892..86f6c1e7 100644 --- a/kagenti-operator/internal/webhook/config/defaults.go +++ b/kagenti-operator/internal/webhook/config/defaults.go @@ -35,6 +35,9 @@ func CompiledDefaults() *PlatformConfig { // Transparent listener port — must match the authbridge proxy-sidecar // preset (listener.transparent_proxy_addr default :8082). TransparentPort: 8082, + // Empty by default: proxy-init auto-detects the iptables backend from + // /proc/modules. Set (e.g. "iptables") to force a backend per-platform. + IptablesCmd: "", }, Resources: ResourcesConfig{ EnvoyProxy: corev1.ResourceRequirements{ diff --git a/kagenti-operator/internal/webhook/config/loader.go b/kagenti-operator/internal/webhook/config/loader.go index 3a2d8167..3d61682e 100644 --- a/kagenti-operator/internal/webhook/config/loader.go +++ b/kagenti-operator/internal/webhook/config/loader.go @@ -199,6 +199,8 @@ func logConfig(cfg *PlatformConfig, source string) { "uid", cfg.Proxy.UID, "inboundProxyPort", cfg.Proxy.InboundProxyPort, "adminPort", cfg.Proxy.AdminPort, + "transparentPort", cfg.Proxy.TransparentPort, + "iptablesCmd", cfg.Proxy.IptablesCmd, ) log.Info("[config] resources.envoyProxy", "requests", cfg.Resources.EnvoyProxy.Requests, diff --git a/kagenti-operator/internal/webhook/config/types.go b/kagenti-operator/internal/webhook/config/types.go index 56a50432..7f2df7fe 100644 --- a/kagenti-operator/internal/webhook/config/types.go +++ b/kagenti-operator/internal/webhook/config/types.go @@ -51,6 +51,14 @@ type ProxyConfig struct { // external TCP egress to. It MUST match the authbridge proxy-sidecar // listener.transparent_proxy_addr (default :8082). TransparentPort int32 `json:"transparentPort" yaml:"transparentPort"` + + // IptablesCmd optionally pins the iptables backend the proxy-init script + // uses, injected as the IPTABLES_CMD env var (omitted when empty). Empty + // (default) lets the script auto-detect from /proc/modules (iptable_nat + // loaded => legacy, as on Kind/kubeadm; absent => nft, as on OpenShift/ + // 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"` } type ResourcesConfig struct { @@ -131,6 +139,15 @@ func (c *PlatformConfig) Validate() error { if c.Proxy.UID < 1 { return fmt.Errorf("proxy.uid must be >= 1 (got %d): the proxy must not run as root and the egress-enforcement exemption keys on this UID", c.Proxy.UID) } + // IptablesCmd, when set, pins the proxy-init iptables backend (IPTABLES_CMD). + // Restrict overrides to the binaries shipped in the proxy-init image so a + // chart typo fails fast at operator startup rather than as a per-injected-pod + // init crash. Empty is the default — proxy-init auto-detects from /proc/modules. + switch c.Proxy.IptablesCmd { + case "", "iptables", "iptables-nft", "iptables-legacy": + 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 c.Images.EnvoyProxy == "" { return fmt.Errorf("images.envoyProxy is required") } diff --git a/kagenti-operator/internal/webhook/config/types_test.go b/kagenti-operator/internal/webhook/config/types_test.go index 542b64d8..2bb86431 100644 --- a/kagenti-operator/internal/webhook/config/types_test.go +++ b/kagenti-operator/internal/webhook/config/types_test.go @@ -29,3 +29,34 @@ func TestValidate_TransparentPort(t *testing.T) { }) } } + +// IptablesCmd pins the proxy-init backend; only the binaries shipped in the +// image (plus "" = auto-detect) are accepted, so a chart typo fails at operator +// startup rather than as a per-injected-pod init crash. +func TestValidate_IptablesCmd(t *testing.T) { + tests := []struct { + name string + cmd string + wantErr bool + }{ + {"empty (auto-detect) ok", "", false}, + {"iptables (nft) ok", "iptables", false}, + {"iptables-nft ok", "iptables-nft", false}, + {"iptables-legacy ok", "iptables-legacy", false}, + {"typo rejected", "iptable", true}, + {"arbitrary rejected", "nft", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := CompiledDefaults() + c.Proxy.IptablesCmd = tt.cmd + err := c.Validate() + if tt.wantErr && err == nil { + t.Errorf("expected validation error, got nil") + } + if !tt.wantErr && err != nil { + t.Errorf("unexpected validation error: %v", err) + } + }) + } +} diff --git a/kagenti-operator/internal/webhook/injector/container_builder.go b/kagenti-operator/internal/webhook/injector/container_builder.go index ba8966a4..c42bfb01 100644 --- a/kagenti-operator/internal/webhook/injector/container_builder.go +++ b/kagenti-operator/internal/webhook/injector/container_builder.go @@ -534,6 +534,14 @@ func (b *ContainerBuilder) BuildProxyInitContainer(mode ProxyInitMode, outboundP return corev1.Container{} } + // Optional explicit iptables backend override (applies to both modes). Empty + // by default: the init script auto-detects from /proc/modules (iptable_nat + // loaded => legacy, else nft). Set b.cfg.Proxy.IptablesCmd (e.g. "iptables" + // on nft-only platforms) to force a backend. + if b.cfg.Proxy.IptablesCmd != "" { + env = append(env, corev1.EnvVar{Name: "IPTABLES_CMD", Value: b.cfg.Proxy.IptablesCmd}) + } + return corev1.Container{ Name: ProxyInitContainerName, Image: b.cfg.Images.ProxyInit, diff --git a/kagenti-operator/internal/webhook/injector/container_builder_test.go b/kagenti-operator/internal/webhook/injector/container_builder_test.go index cc4d937a..84a0eec1 100644 --- a/kagenti-operator/internal/webhook/injector/container_builder_test.go +++ b/kagenti-operator/internal/webhook/injector/container_builder_test.go @@ -336,6 +336,45 @@ func TestBuildProxyInitContainer_EnforceRedirect(t *testing.T) { if _, ok := got["OUTBOUND_PORTS_EXCLUDE"]; ok { t.Error("enforce-redirect must not set OUTBOUND_PORTS_EXCLUDE") } + if _, ok := got["IPTABLES_CMD"]; ok { + t.Error("enforce-redirect must not set IPTABLES_CMD by default (proxy-init auto-detects from /proc/modules)") + } +} + +// IPTABLES_CMD is injected only when Proxy.IptablesCmd is configured, in both +// modes, so the init script's /proc/modules auto-detection stays the default +// and the backend override is strictly opt-in. +func TestBuildProxyInitContainer_IptablesCmd(t *testing.T) { + modes := []ProxyInitMode{ProxyInitModeRedirect, ProxyInitModeEnforceRedirect} + + // Default (empty): env var absent in both modes. + def := NewContainerBuilder(config.CompiledDefaults()) + for _, mode := range modes { + for _, e := range def.BuildProxyInitContainer(mode, "", "").Env { + if e.Name == "IPTABLES_CMD" { + t.Errorf("mode %q: IPTABLES_CMD must be absent when unset, got %q", mode, e.Value) + } + } + } + + // Configured: env var present with the configured value in both modes. + cfg := config.CompiledDefaults() + cfg.Proxy.IptablesCmd = "iptables" + b := NewContainerBuilder(cfg) + for _, mode := range modes { + found := false + for _, e := range b.BuildProxyInitContainer(mode, "", "").Env { + if e.Name == "IPTABLES_CMD" { + found = true + if e.Value != "iptables" { + t.Errorf("mode %q: IPTABLES_CMD = %q, want %q", mode, e.Value, "iptables") + } + } + } + if !found { + t.Errorf("mode %q: IPTABLES_CMD env not set when configured", mode) + } + } } // An unknown mode must fail closed: BuildProxyInitContainer returns a