Skip to content

Commit e3a0491

Browse files
feat: default mTLS to permissive and enable card discovery
Change mTLS default from disabled to permissive so agents get mTLS automatically when SPIRE is available. Enable card discovery and verified fetch by default. Add deprecation warnings for legacy JWS signing flags. Changes: - Add +kubebuilder:default=permissive on MTLSMode field - Add ConditionTypeMTLSReady and AnnotationMTLSMode constants - Set kagenti.io/mtls-mode annotation on pod template in controller - Set MTLS_MODE env var on authbridge containers in webhook - Treat empty MTLSMode as permissive across all resolution paths (controller, pod mutator, envoy template, resolved config) - Flip --enable-card-discovery and --enable-verified-fetch to true - Add startup deprecation warnings for legacy signing flags - Clean up mtls-mode annotation on AgentRuntime deletion - Update tests for new default behavior Jira: RHAIENG-4944 Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
1 parent 17b0e60 commit e3a0491

11 files changed

Lines changed: 100 additions & 37 deletions

File tree

charts/kagenti-operator/crds/agent.kagenti.dev_agentruntimes.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ spec:
118118
type: object
119119
type: object
120120
mtlsMode:
121+
default: permissive
121122
description: |-
122123
MTLSMode selects the mTLS posture between authbridge sidecars on
123124
the proxy-sidecar / lite paths. envoy-sidecar handles transport
@@ -127,8 +128,8 @@ spec:
127128
128129
Three valid values:
129130
130-
disabled Plaintext between sidecars (default).
131-
permissive Inbound: byte-peek listener accepts both TLS and
131+
disabled Plaintext between sidecars.
132+
permissive (default) Inbound: byte-peek listener accepts both TLS and
132133
plaintext on the same port. Outbound: tries TLS,
133134
falls back to plaintext on handshake failure (one-line
134135
WARN log per fallback). Use during rollout.
@@ -137,7 +138,7 @@ spec:
137138
completes.
138139
139140
Resolution: AgentRuntime CR > namespace authbridge-runtime-config
140-
mtls.mode > "disabled". Setting mtlsMode != disabled implicitly
141+
mtls.mode > "permissive". Setting mtlsMode != disabled implicitly
141142
requires SPIRE — the operator auto-enables spire for the workload.
142143
143144
CR-empty vs CR="disabled" are observably different in

kagenti-operator/api/v1alpha1/agentruntime_types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ type AgentRuntimeSpec struct {
8686
//
8787
// Three valid values:
8888
//
89-
// disabled Plaintext between sidecars (default).
90-
// permissive Inbound: byte-peek listener accepts both TLS and
89+
// disabled Plaintext between sidecars.
90+
// permissive (default) Inbound: byte-peek listener accepts both TLS and
9191
// plaintext on the same port. Outbound: tries TLS,
9292
// falls back to plaintext on handshake failure (one-line
9393
// WARN log per fallback). Use during rollout.
@@ -96,7 +96,7 @@ type AgentRuntimeSpec struct {
9696
// completes.
9797
//
9898
// Resolution: AgentRuntime CR > namespace authbridge-runtime-config
99-
// mtls.mode > "disabled". Setting mtlsMode != disabled implicitly
99+
// mtls.mode > "permissive". Setting mtlsMode != disabled implicitly
100100
// requires SPIRE — the operator auto-enables spire for the workload.
101101
//
102102
// CR-empty vs CR="disabled" are observably different in
@@ -111,6 +111,7 @@ type AgentRuntimeSpec struct {
111111
// process start).
112112
//
113113
// +optional
114+
// +kubebuilder:default=permissive
114115
// +kubebuilder:validation:Enum=disabled;permissive;strict
115116
MTLSMode string `json:"mtlsMode,omitempty"`
116117
}

kagenti-operator/cmd/main.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,10 @@ func main() {
171171
flag.StringVar(&mlflowCAFile, "mlflow-ca-file", "",
172172
"Path to PEM-encoded CA bundle for MLflow TLS verification (appended to system pool)")
173173

174-
flag.BoolVar(&enableCardDiscovery, "enable-card-discovery", false,
175-
"Enable automatic agent card discovery from AgentRuntime workloads into status.card")
176-
flag.BoolVar(&enableVerifiedFetch, "enable-verified-fetch", false,
177-
"Enable mTLS-authenticated fetch of agent cards via SPIFFE identity")
174+
flag.BoolVar(&enableCardDiscovery, "enable-card-discovery", true,
175+
"Enable automatic agent card discovery from AgentRuntime workloads into status.card (set to false to disable)")
176+
flag.BoolVar(&enableVerifiedFetch, "enable-verified-fetch", true,
177+
"Enable mTLS-authenticated fetch of agent cards via SPIFFE identity (set to false as kill switch)")
178178
flag.StringVar(&verifiedFetchSpiffeSocket, "verified-fetch-spiffe-socket",
179179
"unix:///spiffe-workload-api/spire-agent.sock",
180180
"SPIFFE Workload API socket path for verified fetch")
@@ -237,6 +237,30 @@ func main() {
237237

238238
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
239239

240+
// Startup info logs for defaults that changed in this release.
241+
if enableCardDiscovery {
242+
setupLog.Info("card discovery enabled by default; set --enable-card-discovery=false to disable")
243+
}
244+
if enableVerifiedFetch {
245+
setupLog.Info("verified fetch enabled by default; set --enable-verified-fetch=false to disable")
246+
}
247+
248+
// Deprecation warnings for legacy flags that are now superseded by
249+
// mTLS defaults (permissive mode auto-enables SPIRE and identity binding).
250+
for _, dep := range []struct {
251+
name string
252+
set bool
253+
}{
254+
{"require-a2a-signature", requireA2ASignature},
255+
{"signature-audit-mode", signatureAuditMode},
256+
{"enforce-network-policies", enforceNetworkPolicies},
257+
} {
258+
if dep.set {
259+
setupLog.Info("DEPRECATED: flag is superseded by mTLS permissive default; will be removed in a future release",
260+
"flag", dep.name)
261+
}
262+
}
263+
240264
ctx := ctrl.SetupSignalHandler()
241265

242266
// ========================================

kagenti-operator/config/crd/bases/agent.kagenti.dev_agentruntimes.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ spec:
118118
type: object
119119
type: object
120120
mtlsMode:
121+
default: permissive
121122
description: |-
122123
MTLSMode selects the mTLS posture between authbridge sidecars on
123124
the proxy-sidecar / lite paths. envoy-sidecar handles transport
@@ -127,8 +128,8 @@ spec:
127128
128129
Three valid values:
129130
130-
disabled Plaintext between sidecars (default).
131-
permissive Inbound: byte-peek listener accepts both TLS and
131+
disabled Plaintext between sidecars.
132+
permissive (default) Inbound: byte-peek listener accepts both TLS and
132133
plaintext on the same port. Outbound: tries TLS,
133134
falls back to plaintext on handshake failure (one-line
134135
WARN log per fallback). Use during rollout.
@@ -137,7 +138,7 @@ spec:
137138
completes.
138139
139140
Resolution: AgentRuntime CR > namespace authbridge-runtime-config
140-
mtls.mode > "disabled". Setting mtlsMode != disabled implicitly
141+
mtls.mode > "permissive". Setting mtlsMode != disabled implicitly
141142
requires SPIRE — the operator auto-enables spire for the workload.
142143
143144
CR-empty vs CR="disabled" are observably different in

kagenti-operator/internal/controller/agentruntime_controller.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ const (
6363
// Value is a JSON array of skill names, set by the kagenti backend or the user.
6464
AnnotationSkills = "kagenti.io/skills"
6565

66+
// AnnotationMTLSMode is the annotation applied to PodTemplateSpec to advertise the
67+
// resolved mTLS posture. Read by authbridge sidecars for observability.
68+
AnnotationMTLSMode = "kagenti.io/mtls-mode"
69+
6670
// AnnotationRestartPending marks a Sandbox that was scaled to 0 and needs
6771
// to be scaled back to 1 on the next reconcile cycle. Two-phase restart
6872
// avoids a race with the Sandbox controller's pod-name annotation.
@@ -72,6 +76,7 @@ const (
7276
ConditionTypeReady = "Ready"
7377
ConditionTypeTargetResolved = "TargetResolved"
7478
ConditionTypeConfigResolved = "ConfigResolved"
79+
ConditionTypeMTLSReady = "MTLSReady"
7580

7681
// AnnotationLastCardFetchHash stores the change-detection key used to skip
7782
// redundant card fetches when the workload's pod template has not changed.
@@ -333,6 +338,12 @@ func (r *AgentRuntimeReconciler) applyWorkloadConfig(ctx context.Context, rt *ag
333338

334339
key := types.NamespacedName{Name: ref.Name, Namespace: rt.Namespace}
335340

341+
// Resolve mTLS mode: CR value takes precedence, default to "permissive".
342+
mtlsMode := rt.Spec.MTLSMode
343+
if mtlsMode == "" {
344+
mtlsMode = "permissive"
345+
}
346+
336347
var configHashChanged bool
337348

338349
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
@@ -348,7 +359,8 @@ func (r *AgentRuntimeReconciler) applyWorkloadConfig(ctx context.Context, rt *ag
348359
alreadyConfigured := currentWorkloadLabels[LabelAgentType] == string(rt.Spec.Type) &&
349360
currentWorkloadLabels[LabelManagedBy] == LabelManagedByValue &&
350361
currentPodLabels[LabelAgentType] == string(rt.Spec.Type) &&
351-
currentPodAnnotations[AnnotationConfigHash] == configHash
362+
currentPodAnnotations[AnnotationConfigHash] == configHash &&
363+
currentPodAnnotations[AnnotationMTLSMode] == mtlsMode
352364

353365
if alreadyConfigured {
354366
return nil
@@ -375,19 +387,21 @@ func (r *AgentRuntimeReconciler) applyWorkloadConfig(ctx context.Context, rt *ag
375387
podLabels[LabelAgentType] = string(rt.Spec.Type)
376388
acc.setPodLabels(acc.obj, podLabels)
377389

378-
// Apply config-hash annotation to PodTemplateSpec
390+
// Apply config-hash and mtls-mode annotations to PodTemplateSpec
379391
podAnnotations := acc.getPodAnnotations(acc.obj)
380392
if podAnnotations == nil {
381393
podAnnotations = make(map[string]string)
382394
}
383395
podAnnotations[AnnotationConfigHash] = configHash
396+
podAnnotations[AnnotationMTLSMode] = mtlsMode
384397
acc.setPodAnnotations(acc.obj, podAnnotations)
385398

386399
logger.Info("Applying config to workload",
387400
"workload", ref.Name,
388401
"kind", ref.Kind,
389402
"type", string(rt.Spec.Type),
390-
"configHash", configHash[:12])
403+
"configHash", configHash[:12],
404+
"mtlsMode", mtlsMode)
391405

392406
return r.Update(ctx, acc.obj)
393407
})
@@ -725,11 +739,12 @@ func (r *AgentRuntimeReconciler) handleDeletion(ctx context.Context, rt *agentv1
725739
delete(podLabels, LabelAgentType)
726740
acc.setPodLabels(acc.obj, podLabels)
727741

728-
// Remove kagenti.io/config-hash from PodTemplateSpec pod annotations.
729-
// This triggers the rolling update that replaces existing injected pods,
730-
// and leaves the workload annotation-clean for any future AR.
742+
// Remove kagenti.io/config-hash and kagenti.io/mtls-mode from PodTemplateSpec
743+
// pod annotations. This triggers the rolling update that replaces existing
744+
// injected pods, and leaves the workload annotation-clean for any future AR.
731745
podAnnotations := acc.getPodAnnotations(acc.obj)
732746
delete(podAnnotations, AnnotationConfigHash)
747+
delete(podAnnotations, AnnotationMTLSMode)
733748
acc.setPodAnnotations(acc.obj, podAnnotations)
734749

735750
logger.Info("Removed kagenti labels and config-hash from workload on AgentRuntime deletion",

kagenti-operator/internal/webhook/injector/agentruntime_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type AgentRuntimeOverrides struct {
5656

5757
// mTLS posture — from .spec.mtlsMode
5858
// Nil = no per-workload override; the namespace's
59-
// authbridge-runtime-config mtls.mode (if set) or "disabled"
59+
// authbridge-runtime-config mtls.mode (if set) or "permissive"
6060
// applies.
6161
MTLSMode *string
6262
}

kagenti-operator/internal/webhook/injector/envoy_template.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,20 @@ func RenderEnvoyConfig(cfg *ResolvedConfig) (string, error) {
7272
return cfg.EnvoyYAML, nil
7373
}
7474

75-
// MTLSEnabled checks both "" and MTLSModeDisabled because
76-
// ResolvedConfig leaves MTLSMode as "" when no source set it
77-
// (CR / namespace ConfigMap / default — see ResolveConfig). The
78-
// resolution chain only fills MTLSMode when something explicitly
79-
// asked for it, so "" means "no opinion → treat as disabled".
75+
// MTLSEnabled: empty string is treated as permissive (mTLS is on
76+
// by default). Only MTLSModeDisabled explicitly disables mTLS.
77+
effectiveMode := cfg.MTLSMode
78+
if effectiveMode == "" {
79+
effectiveMode = MTLSModePermissive
80+
}
8081
data := envoyTemplateData{
8182
AdminPort: cfg.Platform.Proxy.AdminPort,
8283
OutboundPort: cfg.Platform.Proxy.Port,
8384
InboundPort: cfg.Platform.Proxy.InboundProxyPort,
8485
ExtProcPort: defaultExtProcPort,
85-
MTLSEnabled: cfg.MTLSMode != "" && cfg.MTLSMode != MTLSModeDisabled,
86-
MTLSPermissive: cfg.MTLSMode == MTLSModePermissive,
87-
MTLSStrict: cfg.MTLSMode == MTLSModeStrict,
86+
MTLSEnabled: effectiveMode != MTLSModeDisabled,
87+
MTLSPermissive: effectiveMode == MTLSModePermissive,
88+
MTLSStrict: effectiveMode == MTLSModeStrict,
8889
}
8990

9091
var buf bytes.Buffer

kagenti-operator/internal/webhook/injector/envoy_template_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,9 @@ func TestRenderEnvoyConfig_TemplateRendering(t *testing.T) {
7070
}
7171

7272
func TestRenderEnvoyConfig_MTLSDisabled_NoTLSBlocks(t *testing.T) {
73-
// Default / disabled mode — no TLS blocks should render. Locks in
74-
// the existing plaintext shape so a future template edit can't
75-
// silently leak TLS config into pods that didn't ask for it.
76-
for _, mode := range []string{"", MTLSModeDisabled} {
73+
// Explicitly disabled mode — no TLS blocks should render.
74+
// Empty string is now treated as permissive (mTLS on by default).
75+
for _, mode := range []string{MTLSModeDisabled} {
7776
t.Run("mode="+mode, func(t *testing.T) {
7877
cfg := &ResolvedConfig{
7978
Platform: config.CompiledDefaults(),

kagenti-operator/internal/webhook/injector/pod_mutator.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp
257257
}
258258
}
259259
if mtlsMode == "" {
260-
mtlsMode = MTLSModeDisabled
260+
mtlsMode = MTLSModePermissive
261261
mtlsSource = "default"
262262
}
263263
// Defense in depth: the CRD enum check rejects unknown values at
@@ -270,10 +270,10 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp
270270
case MTLSModeDisabled, MTLSModePermissive, MTLSModeStrict:
271271
// recognized, keep as-is
272272
default:
273-
mutatorLog.Info("WARN: unrecognized mtlsMode; defaulting to disabled",
273+
mutatorLog.Info("WARN: unrecognized mtlsMode; defaulting to permissive",
274274
"namespace", namespace, "crName", crName,
275275
"unrecognized", mtlsMode, "source", mtlsSource)
276-
mtlsMode = MTLSModeDisabled
276+
mtlsMode = MTLSModePermissive
277277
mtlsSource = "default-invalid-fallback"
278278
}
279279
mutatorLog.Info("resolved mTLS mode",
@@ -516,6 +516,15 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp
516516
))
517517
}
518518

519+
// Set MTLS_MODE env var on the authbridge container so it knows the
520+
// resolved mTLS posture at runtime.
521+
for i := range podSpec.Containers {
522+
if podSpec.Containers[i].Name == AuthBridgeProxyContainerName {
523+
setOrAddEnv(&podSpec.Containers[i], "MTLS_MODE", mtlsMode)
524+
break
525+
}
526+
}
527+
519528
// Inject HTTP_PROXY env vars into all existing app containers
520529
for i := range podSpec.Containers {
521530
c := &podSpec.Containers[i]
@@ -620,6 +629,14 @@ func (m *PodMutator) InjectAuthBridge(ctx context.Context, podSpec *corev1.PodSp
620629
podSpec.Containers = append(podSpec.Containers, builder.BuildEnvoyProxyContainerWithSpireOption(spireEnabled))
621630
}
622631

632+
// Set MTLS_MODE env var on the envoy-sidecar authbridge container.
633+
for i := range podSpec.Containers {
634+
if podSpec.Containers[i].Name == EnvoyProxyContainerName {
635+
setOrAddEnv(&podSpec.Containers[i], "MTLS_MODE", mtlsMode)
636+
break
637+
}
638+
}
639+
623640
if decision.ProxyInit.Inject && !containerExists(podSpec.InitContainers, ProxyInitContainerName) {
624641
outboundExclude := annotations[OutboundPortsExcludeAnnotation]
625642
inboundExclude := annotations[InboundPortsExcludeAnnotation]

kagenti-operator/internal/webhook/injector/pod_mutator_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ func TestInjectAuthBridge_RespectsExistingServiceAccountName(t *testing.T) {
233233
func TestInjectAuthBridge_NoSACreationWhenSpiffeHelperDisabled(t *testing.T) {
234234
// Spiffe-helper is injected by default for agents. SA creation is skipped
235235
// when spiffe-helper is explicitly opted out via its per-sidecar label.
236-
m := newTestMutator(newAgentRuntime("test-ns", "my-agent"))
236+
// MTLSMode must be set to "disabled" because the default (permissive) would
237+
// auto-enable SPIRE, creating a ServiceAccount regardless of the spiffe-helper label.
238+
rt := newAgentRuntime("test-ns", "my-agent")
239+
rt.Spec.MTLSMode = "disabled"
240+
m := newTestMutator(rt)
237241
ctx := context.Background()
238242

239243
podSpec := &corev1.PodSpec{}

0 commit comments

Comments
 (0)