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
15 changes: 14 additions & 1 deletion charts/kagenti-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ featureGates:
# * authbridgeLite (lite mode): authbridge-lite (jwt-validation + token-exchange only,
# parsers dropped) + spiffe-helper. Same listener layout
# as authbridge; for size-constrained deployments.
# proxy-init applies to envoy-sidecar mode only.
# proxy-init applies to envoy-sidecar mode (transparent redirect) and to
# proxy-sidecar mode when proxy.egressEnforcement is "enforce" (enforce-drop).
defaults:
images:
envoyProxy: ghcr.io/kagenti/kagenti-extensions/authbridge-envoy:latest
Expand All @@ -189,6 +190,18 @@ 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.
clusterCIDRs:
- "10.0.0.0/8"

# Resource defaults (conservative for dev)
# Note: requests must be <= limits
Expand Down
6 changes: 6 additions & 0 deletions kagenti-operator/internal/webhook/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ func CompiledDefaults() *PlatformConfig {
UID: 1337,
InboundProxyPort: 15124,
AdminPort: 9901,
// Off by default — proxy-sidecar stays cooperative (HTTP_PROXY only)
// until an operator opts in. Migrate to "enforce" in a future release.
EgressEnforcement: EgressEnforcementOff,
// 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"},
},
Resources: ResourcesConfig{
EnvoyProxy: corev1.ResourceRequirements{
Expand Down
60 changes: 60 additions & 0 deletions kagenti-operator/internal/webhook/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"fmt"
"net"

corev1 "k8s.io/api/core/v1"
)
Expand Down Expand Up @@ -40,11 +41,37 @@ 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"`

// 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".
ClusterCIDRs []string `json:"clusterCIDRs" yaml:"clusterCIDRs"`
}

type ResourcesConfig struct {
Expand Down Expand Up @@ -83,6 +110,11 @@ func (c *PlatformConfig) DeepCopy() *PlatformConfig {
copy(result.TokenExchange.DefaultScopes, c.TokenExchange.DefaultScopes)
}

if c.Proxy.ClusterCIDRs != nil {
result.Proxy.ClusterCIDRs = make([]string, len(c.Proxy.ClusterCIDRs))
copy(result.Proxy.ClusterCIDRs, c.Proxy.ClusterCIDRs)
}

// 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)
Expand Down Expand Up @@ -119,6 +151,34 @@ 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
// 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 {

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.

nit: net.ParseCIDR(cidr) per entry catches malformed syntax (good), but doesn't reject overlapping ranges or warn on IPv6 in IPv4-only deployments. Probably intentional — overlapping CIDRs are fine for iptables — but worth a one-line comment so future readers don't assume the validation is exhaustive:

// Syntactic validation only — overlapping ranges and IPv4/IPv6 mixing are
// accepted (iptables handles both); we only reject malformed strings here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the clarifying comment in d280093 — validation is syntactic only; overlapping ranges and v4/v6 mixing are accepted (iptables and the init script split v4/v6 themselves).

return fmt.Errorf("proxy.clusterCIDRs entry %q is not a valid CIDR: %w", cidr, err)
}
}
}
if c.Images.EnvoyProxy == "" {
return fmt.Errorf("images.envoyProxy is required")
}
Expand Down
51 changes: 51 additions & 0 deletions kagenti-operator/internal/webhook/config/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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
// (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
}
tests := []struct {
name string
mutate func(*PlatformConfig)
wantErr bool
}{
{"default CIDRs ok", func(c *PlatformConfig) {}, false},
{"empty CIDRs rejected", func(c *PlatformConfig) { c.Proxy.ClusterCIDRs = nil }, true},
{"malformed CIDR rejected", func(c *PlatformConfig) {
c.Proxy.ClusterCIDRs = []string{"10.0.0.0/8", "garbage"}
}, true},
{"valid OCP-shaped CIDRs ok", func(c *PlatformConfig) {
c.Proxy.ClusterCIDRs = []string{"10.128.0.0/14", "172.30.0.0/16"}
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := base()
tt.mutate(c)
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)
}
})
}
}

// 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)
}
}
17 changes: 17 additions & 0 deletions kagenti-operator/internal/webhook/injector/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ const (
ClientAuthTypeFederatedJWT = "federated-jwt"
)

// ProxyInitMode is the iptables strategy BuildProxyInitContainer passes to
// init-iptables.sh. It is a named type so callsites read intent-fully and the
// compiler rejects passing an unrelated typed value; BuildProxyInitContainer
// additionally fails closed on any unknown value (see its default branch),
// since an untyped string literal can still convert to this type at a callsite.
type ProxyInitMode string

// proxy-init MODE values.
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"
)

// 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
Expand Down
124 changes: 80 additions & 44 deletions kagenti-operator/internal/webhook/injector/container_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,13 @@ func (b *ContainerBuilder) BuildProxySidecarContainerWithPorts(spireEnabled bool
},
Resources: b.cfg.Resources.AuthBridge,
SecurityContext: &corev1.SecurityContext{
RunAsUser: ptr.To(int64(1001)),
// Run as the dedicated proxy UID (default 1337), the SAME value the
// proxy-init enforce-drop 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.
RunAsUser: ptr.To(b.cfg.Proxy.UID),
RunAsGroup: ptr.To(b.cfg.Proxy.UID),
RunAsNonRoot: ptr.To(true),
AllowPrivilegeEscalation: ptr.To(false),
},
Expand Down Expand Up @@ -448,51 +454,81 @@ func (b *ContainerBuilder) buildEnvoyProxyEnvLegacy() []corev1.EnvVar {
const mandatoryOutboundExclude = "8080"

// BuildProxyInitContainer creates the proxy-init container.
// outboundPortsExclude is a comma-separated list of additional ports to
// exclude from outbound interception (mandatory 8080 is always included).
// inboundPortsExclude is a comma-separated list of ports to exclude from
// inbound interception (only set when non-empty). Both come from the
// kagenti.io/outbound-ports-exclude and kagenti.io/inbound-ports-exclude
// pod annotations.
func (b *ContainerBuilder) BuildProxyInitContainer(outboundPortsExclude, inboundPortsExclude string) corev1.Container {
outboundValue := buildOutboundExcludeValue(outboundPortsExclude)
inboundValue := buildPortExcludeValue(inboundPortsExclude, "inbound-ports-exclude")

builderLog.Info("building ProxyInit Container",
"resolvedOutboundPortsExclude", outboundValue,
"resolvedInboundPortsExclude", inboundValue)

env := []corev1.EnvVar{
{
Name: "PROXY_PORT",
Value: fmt.Sprintf("%d", b.cfg.Proxy.Port),
},
{
Name: "INBOUND_PROXY_PORT",
Value: fmt.Sprintf("%d", b.cfg.Proxy.InboundProxyPort),
},
{
Name: "PROXY_UID",
Value: fmt.Sprintf("%d", b.cfg.Proxy.UID),
},
{
Name: "OUTBOUND_PORTS_EXCLUDE",
Value: outboundValue,
},
{
Name: "POD_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "status.podIP",
// mode selects the interception strategy the init script runs:
// - "redirect" (envoy-sidecar): transparently REDIRECT pod traffic to the
// 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).
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.
clusterCIDRs := strings.Join(b.cfg.Proxy.ClusterCIDRs, ",")
env = []corev1.EnvVar{
{Name: "MODE", Value: string(ProxyInitModeEnforceDrop)},
{Name: "PROXY_UID", Value: fmt.Sprintf("%d", b.cfg.Proxy.UID)},
{Name: "CLUSTER_CIDRS", Value: clusterCIDRs},
}
builderLog.Info("building ProxyInit Container",
"mode", "enforce-drop",
"proxyUID", b.cfg.Proxy.UID,
"clusterCIDRs", clusterCIDRs)
case ProxyInitModeRedirect:
outboundValue := buildOutboundExcludeValue(outboundPortsExclude)
inboundValue := buildPortExcludeValue(inboundPortsExclude, "inbound-ports-exclude")

builderLog.Info("building ProxyInit Container",
"mode", "redirect",
"resolvedOutboundPortsExclude", outboundValue,
"resolvedInboundPortsExclude", inboundValue)

env = []corev1.EnvVar{
{
Name: "PROXY_PORT",
Value: fmt.Sprintf("%d", b.cfg.Proxy.Port),
},
{
Name: "INBOUND_PROXY_PORT",
Value: fmt.Sprintf("%d", b.cfg.Proxy.InboundProxyPort),
},
{
Name: "PROXY_UID",
Value: fmt.Sprintf("%d", b.cfg.Proxy.UID),
},
{
Name: "OUTBOUND_PORTS_EXCLUDE",
Value: outboundValue,
},
{
Name: "POD_IP",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "status.podIP",
},
},
},
},
}
if inboundValue != "" {
env = append(env, corev1.EnvVar{
Name: "INBOUND_PORTS_EXCLUDE",
Value: inboundValue,
})
}
if inboundValue != "" {
env = append(env, corev1.EnvVar{
Name: "INBOUND_PORTS_EXCLUDE",
Value: inboundValue,
})
}
default:
// Fail closed. An unknown mode must NOT silently degrade to redirect:
// that would ship a proxy-init with no egress guard — fail-open, the
// worst outcome for a fail-closed control. Return a zero-value container
// so injection breaks visibly (empty name/image → admission/scheduler
// rejects it) rather than passing a security-defeating container through.
builderLog.Error(nil, "unknown proxy-init mode; refusing to build container (fail closed)", "mode", mode)
return corev1.Container{}
}

return corev1.Container{
Expand Down
Loading
Loading