Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions charts/kagenti-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ featureGates:
# parsers dropped) + spiffe-helper. Same listener layout
# as authbridge; for size-constrained deployments.
# proxy-init applies to envoy-sidecar mode (transparent redirect) and to
# proxy-sidecar mode when proxy.egressEnforcement is "enforce" (enforce-drop).
# proxy-sidecar / lite mode (always-on enforce-redirect egress capture).
defaults:
images:
envoyProxy: ghcr.io/kagenti/kagenti-extensions/authbridge-envoy:latest
Expand All @@ -203,16 +203,15 @@ defaults:
uid: 1337
adminPort: 9901
inboundProxyPort: 15124
# Proxy-sidecar fail-closed egress enforcement. "off" (default) keeps egress
# cooperative (HTTP_PROXY only); "enforce" injects a proxy-init enforce-drop
# guard so direct egress is dropped. envoy-sidecar is unaffected (it already
# enforces via transparent redirect).
egressEnforcement: "off"
# In-cluster CIDRs (pods/services/DNS) allowed direct by the enforce-drop
# guard; everything else egressing the pod is dropped. The default is
# Kind-shaped (pods 10.244/16 + services 10.96/16). OpenShift/EKS MUST
# override — e.g. OCP services 172.30.0.0/16 (outside 10/8) + pods
# 10.128.0.0/14 — or in-cluster service traffic will be dropped.
# Forward proxy's transparent listener port — the REDIRECT target for the
# always-on enforce-redirect egress guard (proxy-sidecar / lite). MUST match
# the authbridge listener.transparent_proxy_addr (default :8082).
transparentPort: 8082
# In-cluster CIDRs (pods/services/DNS) left direct by the enforce-redirect
# guard; external TCP is REDIRECTed to the transparent listener and external
# non-TCP is dropped. The default is Kind-shaped (pods 10.244/16 + services
# 10.96/16). OpenShift/EKS MUST override — e.g. OCP services 172.30.0.0/16
# (outside 10/8) + pods 10.128.0.0/14 — or in-cluster service traffic breaks.
clusterCIDRs:
- "10.0.0.0/8"

Expand Down
6 changes: 3 additions & 3 deletions kagenti-operator/internal/webhook/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func CompiledDefaults() *PlatformConfig {
UID: 1337,
InboundProxyPort: 15124,
AdminPort: 9901,
// Off by defaultproxy-sidecar stays cooperative (HTTP_PROXY only)
// until an operator opts in. Migrate to "enforce" in a future release.
EgressEnforcement: EgressEnforcementOff,
// Transparent listener portmust match the authbridge proxy-sidecar
// preset (listener.transparent_proxy_addr default :8082).
TransparentPort: 8082,
// Kind-shaped default (pods 10.244/16 + services 10.96/16). OCP/EKS
// MUST override (see ProxyConfig.ClusterCIDRs doc).
ClusterCIDRs: []string{"10.0.0.0/8"},
Expand Down
79 changes: 33 additions & 46 deletions kagenti-operator/internal/webhook/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,36 +41,26 @@ type ImageConfig struct {
PullPolicy corev1.PullPolicy `json:"pullPolicy" yaml:"pullPolicy"`
}

// EgressEnforcement values for ProxyConfig.EgressEnforcement.
const (
EgressEnforcementOff = "off"
EgressEnforcementEnforce = "enforce"
)

type ProxyConfig struct {
Port int32 `json:"port" yaml:"port"`
UID int64 `json:"uid" yaml:"uid"`
InboundProxyPort int32 `json:"inboundProxyPort" yaml:"inboundProxyPort"`
AdminPort int32 `json:"adminPort" yaml:"adminPort"`

// EgressEnforcement controls the proxy-sidecar fail-closed egress guard.
// "off" (default): the workload routes egress through the forward proxy
// via HTTP_PROXY only — cooperative and bypassable.
// "enforce": a proxy-init container installs the enforce-drop iptables
// guard, forcing all external egress through the forward proxy regardless
// of whether the workload honors HTTP_PROXY.
// envoy-sidecar mode is unaffected — it already enforces egress structurally
// via the transparent redirect, so this knob is consulted only on the
// proxy-sidecar / lite path. Migrate the default off->enforce in a future
// release once ClusterCIDRs sourcing is validated across distros.
EgressEnforcement string `json:"egressEnforcement" yaml:"egressEnforcement"`
// TransparentPort is the forward proxy's transparent listener port — the
// REDIRECT target the enforce-redirect proxy-init guard sends captured
// external TCP egress to. It MUST match the authbridge proxy-sidecar
// listener.transparent_proxy_addr (default :8082).
TransparentPort int32 `json:"transparentPort" yaml:"transparentPort"`

// ClusterCIDRs are the in-cluster ranges (pods / services / DNS) that the
// enforce-drop guard allows direct; everything else egressing the pod is
// dropped. The default 10.0.0.0/8 is Kind-shaped (pods 10.244/16 + services
// 10.96/16). OCP/EKS MUST override this (e.g. OCP services 172.30.0.0/16,
// pods 10.128.0.0/14 — 172.30/16 is outside 10/8) or in-cluster service
// traffic will be dropped. Only used when EgressEnforcement == "enforce".
// enforce-redirect guard allows direct; external TCP is REDIRECTed to the
// transparent listener and external non-TCP is dropped. The default
// 10.0.0.0/8 is Kind-shaped (pods 10.244/16 + services 10.96/16). OCP/EKS
// MUST override this (e.g. OCP services 172.30.0.0/16, pods 10.128.0.0/14 —
// 172.30/16 is outside 10/8) or in-cluster service traffic will be dropped.
// Egress enforcement is always-on for proxy-sidecar / lite, so this is
// always consumed there; envoy-sidecar (transparent redirect) does not use it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): EgressEnforcement is removed from the values schema, but the config loader uses plain yaml.Unmarshal (no KnownFields(true)) — so a stale proxy.egressEnforcement: left in an operator's existing values override is silently ignored rather than erroring. That's the benign failure mode (enforcement just becomes always-on anyway), so no upgrade-time crash. Still worth a one-liner in the release/upgrade notes so operators aren't surprised the knob no longer does anything.

ClusterCIDRs []string `json:"clusterCIDRs" yaml:"clusterCIDRs"`
}

Expand Down Expand Up @@ -151,32 +141,29 @@ func (c *PlatformConfig) Validate() error {
if c.Proxy.AdminPort < 1024 || c.Proxy.AdminPort > 65535 {
return fmt.Errorf("proxy.adminPort must be between 1024 and 65535")
}
// The enforce-drop guard exempts this UID (--uid-owner) and the proxy
if c.Proxy.TransparentPort < 1024 || c.Proxy.TransparentPort > 65535 {
return fmt.Errorf("proxy.transparentPort must be between 1024 and 65535")
}
// The enforce-redirect guard exempts this UID (--uid-owner) and the proxy
// container runs as it; it must be a real non-root user.
if c.Proxy.UID < 1 {
return fmt.Errorf("proxy.uid must be >= 1 (got %d): the proxy must not run as root and the enforce-drop exemption keys on this UID", c.Proxy.UID)
}
switch c.Proxy.EgressEnforcement {
case "", EgressEnforcementOff, EgressEnforcementEnforce:
default:
return fmt.Errorf("proxy.egressEnforcement must be \"off\" or \"enforce\" (got %q)", c.Proxy.EgressEnforcement)
}
// When enforce is on, ClusterCIDRs drive the only in-cluster allowance in the
// enforce-drop guard. Validate them at load time so a misconfig fails fast with
// a clear message rather than: (a) an empty list silently falling back to the
// Kind-shaped 10.0.0.0/8 default in init-iptables.sh, or (b) a malformed entry
// crashing the proxy-init container under `set -e` with a cryptic iptables error.
if c.Proxy.EgressEnforcement == EgressEnforcementEnforce {
if len(c.Proxy.ClusterCIDRs) == 0 {
return fmt.Errorf("proxy.clusterCIDRs must be non-empty when proxy.egressEnforcement is %q (set the cluster's pod+service CIDRs)", EgressEnforcementEnforce)
}
// Syntactic validation only — overlapping ranges and IPv4/IPv6 mixing are
// accepted (iptables handles both, and the init script splits v4/v6 itself);
// we only reject malformed strings here.
for _, cidr := range c.Proxy.ClusterCIDRs {
if _, _, err := net.ParseCIDR(cidr); err != nil {
return fmt.Errorf("proxy.clusterCIDRs entry %q is not a valid CIDR: %w", cidr, err)
}
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)
}
// ClusterCIDRs drive the only in-cluster allowance in the enforce-redirect
// guard, which is always-on for proxy-sidecar / lite. Validate at load time so
// a misconfig fails fast with a clear message rather than: (a) an empty list
// silently falling back to the Kind-shaped 10.0.0.0/8 default in
// init-iptables.sh, or (b) a malformed entry crashing the proxy-init container
// under `set -e` with a cryptic iptables error.
if len(c.Proxy.ClusterCIDRs) == 0 {
return fmt.Errorf("proxy.clusterCIDRs must be non-empty (set the cluster's pod+service CIDRs)")
}
// Syntactic validation only — overlapping ranges and IPv4/IPv6 mixing are
// accepted (iptables handles both, and the init script splits v4/v6 itself);
// we only reject malformed strings here.
for _, cidr := range c.Proxy.ClusterCIDRs {
if _, _, err := net.ParseCIDR(cidr); err != nil {
return fmt.Errorf("proxy.clusterCIDRs entry %q is not a valid CIDR: %w", cidr, err)
}
}
if c.Images.EnvoyProxy == "" {
Expand Down
46 changes: 30 additions & 16 deletions kagenti-operator/internal/webhook/config/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ package config

import "testing"

// When egressEnforcement is "enforce", ClusterCIDRs must be present and valid —
// an empty list (silent 10/8 fallback in the init script) or a malformed entry
// ClusterCIDRs drive the only in-cluster allowance in the always-on
// enforce-redirect guard, so they must always be present and valid — an empty
// list (silent 10/8 fallback in the init script) or a malformed entry
// (init-container CrashLoop under set -e) must be rejected at config load.
func TestValidate_ClusterCIDRs_EnforceMode(t *testing.T) {
base := func() *PlatformConfig {
c := CompiledDefaults()
c.Proxy.EgressEnforcement = EgressEnforcementEnforce
return c
}
func TestValidate_ClusterCIDRs(t *testing.T) {
tests := []struct {
name string
mutate func(*PlatformConfig)
Expand All @@ -27,7 +23,7 @@ func TestValidate_ClusterCIDRs_EnforceMode(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := base()
c := CompiledDefaults()
tt.mutate(c)
err := c.Validate()
if tt.wantErr && err == nil {
Expand All @@ -40,12 +36,30 @@ func TestValidate_ClusterCIDRs_EnforceMode(t *testing.T) {
}
}

// When enforcement is off, ClusterCIDRs is unused and must not be validated.
func TestValidate_ClusterCIDRs_OffMode_NotValidated(t *testing.T) {
c := CompiledDefaults()
c.Proxy.EgressEnforcement = EgressEnforcementOff
c.Proxy.ClusterCIDRs = []string{"garbage"}
if err := c.Validate(); err != nil {
t.Errorf("off mode must not validate ClusterCIDRs, got: %v", err)
// TransparentPort is the REDIRECT target for the enforce-redirect guard; it must
// be a valid, non-privileged port (the proxy binds it as a non-root user).
func TestValidate_TransparentPort(t *testing.T) {
tests := []struct {
name string
port int32
wantErr bool
}{
{"default 8082 ok", 8082, false},
{"zero rejected", 0, true},
{"privileged rejected", 80, true},
{"too large rejected", 70000, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := CompiledDefaults()
c.Proxy.TransparentPort = tt.port
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)
}
})
}
}
8 changes: 5 additions & 3 deletions kagenti-operator/internal/webhook/injector/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ const (
// ProxyInitModeRedirect transparently REDIRECTs pod traffic to the Envoy
// listeners (envoy-sidecar mode).
ProxyInitModeRedirect ProxyInitMode = "redirect"
// ProxyInitModeEnforceDrop installs the fail-closed egress guard that DROPs
// any egress bypassing the forward proxy (proxy-sidecar egress enforcement).
ProxyInitModeEnforceDrop ProxyInitMode = "enforce-drop"
// ProxyInitModeEnforceRedirect installs the fail-closed egress guard that
// REDIRECTs external TCP bypassing the forward proxy to AuthBridge's
// transparent listener (captured, not dropped) and DROPs non-TCP external
// egress. Always-on for proxy-sidecar / lite.
ProxyInitModeEnforceRedirect ProxyInitMode = "enforce-redirect"
)

// mTLS modes for the proxy-sidecar / lite paths. Selected per workload
Expand Down
26 changes: 15 additions & 11 deletions kagenti-operator/internal/webhook/injector/container_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func (b *ContainerBuilder) BuildProxySidecarContainerWithPorts(spireEnabled bool
Resources: b.cfg.Resources.AuthBridge,
SecurityContext: &corev1.SecurityContext{
// Run as the dedicated proxy UID (default 1337), the SAME value the
// proxy-init enforce-drop guard exempts via `--uid-owner $PROXY_UID`.
// proxy-init enforce-redirect guard exempts via `--uid-owner $PROXY_UID`.
// Deriving both from b.cfg.Proxy.UID keeps the exempted UID in lockstep
// with the process it exempts (a hardcoded literal could drift and
// silently break the proxy's own egress). Matches the envoy builder.
Expand Down Expand Up @@ -459,26 +459,30 @@ const mandatoryOutboundExclude = "8080"
// Envoy listeners. outboundPortsExclude / inboundPortsExclude (from the
// kagenti.io/outbound-ports-exclude and kagenti.io/inbound-ports-exclude
// pod annotations; mandatory 8080 always included outbound) tune it.
// - "enforce-drop" (proxy-sidecar): a fail-closed egress guard that DROPs any
// egress bypassing the forward proxy. Driven by PROXY_UID + CLUSTER_CIDRS;
// the exclude args do not apply (the script ignores them in this mode).
// - "enforce-redirect" (proxy-sidecar): a fail-closed egress guard that
// REDIRECTs external TCP bypassing the forward proxy to the transparent
// listener (TRANSPARENT_PORT) and DROPs non-TCP. Driven by PROXY_UID +
// CLUSTER_CIDRS + TRANSPARENT_PORT; the exclude args do not apply.
func (b *ContainerBuilder) BuildProxyInitContainer(mode ProxyInitMode, outboundPortsExclude, inboundPortsExclude string) corev1.Container {
var env []corev1.EnvVar
switch mode {
case ProxyInitModeEnforceDrop:
// PROXY_UID is exempted from the DROP and MUST match the proxy
// container's RunAsUser (both derive from b.cfg.Proxy.UID). CLUSTER_CIDRS
// are allowed direct; everything else egressing the pod is dropped.
// POD_IP and the redirect-only exclude vars are unused in this mode.
case ProxyInitModeEnforceRedirect:
// PROXY_UID is exempted (its egress is not redirected) and MUST match the
// proxy container's RunAsUser (both derive from b.cfg.Proxy.UID).
// CLUSTER_CIDRS are allowed direct; external TCP is REDIRECTed to
// TRANSPARENT_PORT (the proxy's transparent listener), external non-TCP is
// dropped. The redirect-only exclude vars are unused in this mode.
clusterCIDRs := strings.Join(b.cfg.Proxy.ClusterCIDRs, ",")
env = []corev1.EnvVar{
{Name: "MODE", Value: string(ProxyInitModeEnforceDrop)},
{Name: "MODE", Value: string(ProxyInitModeEnforceRedirect)},
{Name: "PROXY_UID", Value: fmt.Sprintf("%d", b.cfg.Proxy.UID)},
{Name: "CLUSTER_CIDRS", Value: clusterCIDRs},
{Name: "TRANSPARENT_PORT", Value: fmt.Sprintf("%d", b.cfg.Proxy.TransparentPort)},
}
builderLog.Info("building ProxyInit Container",
"mode", "enforce-drop",
"mode", "enforce-redirect",
"proxyUID", b.cfg.Proxy.UID,
"transparentPort", b.cfg.Proxy.TransparentPort,
"clusterCIDRs", clusterCIDRs)
case ProxyInitModeRedirect:
outboundValue := buildOutboundExcludeValue(outboundPortsExclude)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ func TestBuildProxyInitContainer_WithBothExcludes(t *testing.T) {
}

// The proxy-sidecar container must run as the configured Proxy.UID — the same
// value the enforce-drop guard exempts via --uid-owner. If these drift, the
// proxy's own egress would be dropped (or the agent would share the exempt UID).
// value the enforce-redirect guard exempts via --uid-owner. If these drift, the
// proxy's own egress would be redirected back (or the agent would share the exempt UID).
func TestBuildProxySidecarContainer_RunAsProxyUID(t *testing.T) {
cfg := config.CompiledDefaults()
builder := NewContainerBuilder(cfg)
Expand All @@ -304,34 +304,37 @@ func TestBuildProxySidecarContainer_RunAsProxyUID(t *testing.T) {
}
}

// enforce-drop mode emits MODE / PROXY_UID / CLUSTER_CIDRS and none of the
// redirect-only vars (POD_IP, OUTBOUND_PORTS_EXCLUDE).
func TestBuildProxyInitContainer_EnforceDrop(t *testing.T) {
// enforce-redirect mode emits MODE / PROXY_UID / CLUSTER_CIDRS / TRANSPARENT_PORT
// and none of the redirect-only vars (POD_IP, OUTBOUND_PORTS_EXCLUDE).
func TestBuildProxyInitContainer_EnforceRedirect(t *testing.T) {
cfg := config.CompiledDefaults()
builder := NewContainerBuilder(cfg)
container := builder.BuildProxyInitContainer("enforce-drop", "", "")
container := builder.BuildProxyInitContainer("enforce-redirect", "", "")

got := map[string]string{}
for _, e := range container.Env {
if e.ValueFrom != nil {
t.Errorf("enforce-drop env %q must be a literal, not ValueFrom", e.Name)
t.Errorf("enforce-redirect env %q must be a literal, not ValueFrom", e.Name)
}
got[e.Name] = e.Value
}
if got["MODE"] != "enforce-drop" {
t.Errorf("MODE = %q, want enforce-drop", got["MODE"])
if got["MODE"] != "enforce-redirect" {
t.Errorf("MODE = %q, want enforce-redirect", got["MODE"])
}
if want := strconv.FormatInt(cfg.Proxy.UID, 10); got["PROXY_UID"] != want {
t.Errorf("PROXY_UID = %q, want %q", got["PROXY_UID"], want)
}
if want := strings.Join(cfg.Proxy.ClusterCIDRs, ","); got["CLUSTER_CIDRS"] != want {
t.Errorf("CLUSTER_CIDRS = %q, want %q", got["CLUSTER_CIDRS"], want)
}
if want := strconv.FormatInt(int64(cfg.Proxy.TransparentPort), 10); got["TRANSPARENT_PORT"] != want {
t.Errorf("TRANSPARENT_PORT = %q, want %q", got["TRANSPARENT_PORT"], want)
}
if _, ok := got["POD_IP"]; ok {
t.Error("enforce-drop must not set POD_IP")
t.Error("enforce-redirect must not set POD_IP")
}
if _, ok := got["OUTBOUND_PORTS_EXCLUDE"]; ok {
t.Error("enforce-drop must not set OUTBOUND_PORTS_EXCLUDE")
t.Error("enforce-redirect must not set OUTBOUND_PORTS_EXCLUDE")
}
}

Expand Down
18 changes: 9 additions & 9 deletions kagenti-operator/internal/webhook/injector/pod_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,18 +525,18 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp
injectHTTPProxyEnv(c, forwardProxyPort)
}

// Fail-closed egress enforcement (opt-in via proxy.egressEnforcement,
// default "off"). HTTP_PROXY above is cooperative — an app that ignores
// it egresses directly. When enforcement is on, inject the proxy-init
// enforce-drop guard so any direct egress is dropped. envoy-sidecar
// already enforces structurally via transparent redirect, so this is the
// Fail-closed egress enforcement (always-on 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 currentConfig.Proxy.EgressEnforcement == config.EgressEnforcementEnforce &&
!containerExists(podSpec.InitContainers, ProxyInitContainerName) {
if !containerExists(podSpec.InitContainers, ProxyInitContainerName) {
podSpec.InitContainers = append(podSpec.InitContainers,
builder.BuildProxyInitContainer(ProxyInitModeEnforceDrop, "", ""))
mutatorLog.Info("proxy-sidecar egress enforcement enabled (enforce-drop)",
builder.BuildProxyInitContainer(ProxyInitModeEnforceRedirect, "", ""))
mutatorLog.Info("proxy-sidecar egress enforcement enabled (enforce-redirect)",
"namespace", namespace, "crName", crName)
}

Expand Down
Loading
Loading