Skip to content
Open
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
4 changes: 2 additions & 2 deletions examples/modelconfig-with-tls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -386,5 +386,5 @@ roleRef:
# 6. Troubleshooting:
# - See https://kagent.dev/docs for detailed debugging steps
# - Check agent logs: kubectl logs deployment/agent-<name>
# - Verify Secret is mounted: kubectl exec deployment/agent-<name> -- ls /etc/ssl/certs/custom/
# - Test certificate: kubectl exec deployment/agent-<name> -- openssl x509 -in /etc/ssl/certs/custom/ca.crt -text -noout
# - Verify Secret is mounted: kubectl exec deployment/agent-<name> -- ls /etc/ssl/certs/custom/corp-ca/
# - Test certificate: kubectl exec deployment/agent-<name> -- openssl x509 -in /etc/ssl/certs/custom/corp-ca/ca.crt -text -noout
43 changes: 21 additions & 22 deletions go/api/config/crd/bases/kagent.dev_modelconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -652,17 +652,18 @@ spec:
properties:
caCertSecretKey:
description: |-
CACertSecretKey is the key within the Secret that contains the CA certificate data.
This field follows the same pattern as APIKeySecretKey.
Required when CACertSecretRef is set (unless DisableVerify is true).
CACertSecretKey is the key within the Secret that contains the
CA certificate data (PEM-encoded). Required when CACertSecretRef
is set (unless DisableVerify is true).
type: string
caCertSecretRef:
description: |-
CACertSecretRef is a reference to a Kubernetes Secret containing
CA certificate(s) in PEM format. The Secret must be in the same
namespace as the ModelConfig.
When set, the certificate will be used to verify the provider's SSL certificate.
This field follows the same pattern as APIKeySecret.
namespace as the resource referencing it (ModelConfig,
RemoteMCPServer, or any future consumer of TLSConfig).
When set, the certificate will be used to verify the upstream's
SSL certificate.
type: string
disableSystemCAs:
default: false
Expand All @@ -682,6 +683,20 @@ spec:
Production deployments MUST use proper certificates.
type: boolean
type: object
x-kubernetes-validations:
- message: caCertSecretKey requires caCertSecretRef
rule: '!(has(self.caCertSecretKey) && size(self.caCertSecretKey)
> 0 && (!has(self.caCertSecretRef) || size(self.caCertSecretRef)
== 0))'
- message: caCertSecretRef requires caCertSecretKey
rule: '!(has(self.caCertSecretRef) && size(self.caCertSecretRef)
> 0 && (!has(self.caCertSecretKey) || size(self.caCertSecretKey)
== 0))'
- message: disableSystemCAs requires caCertSecretRef or disableVerify
(trust-nothing config rejects every upstream)
rule: '!(has(self.disableSystemCAs) && self.disableSystemCAs &&
(!has(self.disableVerify) || !self.disableVerify) && (!has(self.caCertSecretRef)
|| size(self.caCertSecretRef) == 0))'
required:
- model
type: object
Expand Down Expand Up @@ -719,22 +734,6 @@ spec:
rule: '!(has(self.apiKeyPassthrough) && self.apiKeyPassthrough && (self.provider
== ''Gemini'' || self.provider == ''GeminiVertexAI'' || self.provider
== ''AnthropicVertexAI''))'
- message: caCertSecretKey requires caCertSecretRef
rule: '!(has(self.tls) && has(self.tls.caCertSecretKey) && size(self.tls.caCertSecretKey)
> 0 && (!has(self.tls.caCertSecretRef) || size(self.tls.caCertSecretRef)
== 0))'
- message: caCertSecretKey requires caCertSecretRef (unless disableVerify
is true)
rule: '!(has(self.tls) && (!has(self.tls.disableVerify) || !self.tls.disableVerify)
&& has(self.tls.caCertSecretKey) && size(self.tls.caCertSecretKey)
> 0 && (!has(self.tls.caCertSecretRef) || size(self.tls.caCertSecretRef)
== 0))'
- message: caCertSecretRef requires caCertSecretKey (unless disableVerify
is true)
rule: '!(has(self.tls) && (!has(self.tls.disableVerify) || !self.tls.disableVerify)
&& has(self.tls.caCertSecretRef) && size(self.tls.caCertSecretRef)
> 0 && (!has(self.tls.caCertSecretKey) || size(self.tls.caCertSecretKey)
== 0))'
- message: openAI.tokenExchange requires apiKeySecret (the service account
secret)
rule: '!(has(self.openAI) && has(self.openAI.tokenExchange) && (!has(self.apiKeySecret)
Expand Down
64 changes: 64 additions & 0 deletions go/api/config/crd/bases/kagent.dev_remotemcpservers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,64 @@ spec:
timeout:
default: 30s
type: string
tls:
description: |-
TLS configuration for the upstream MCP server connection.
Use this for HTTPS upstreams that present a certificate the agent's
system trust store does not include (corporate CA, self-signed cert
on a test fixture, internal MCP gateway). Reuses the same TLSConfig
type as ModelConfig.spec.tls, with identical semantics: disableVerify
turns off certificate validation entirely, caCertSecretRef +
caCertSecretKey point at a PEM bundle Secret in the same namespace,
and disableSystemCAs trusts only the named bundle.
properties:
caCertSecretKey:
description: |-
CACertSecretKey is the key within the Secret that contains the
CA certificate data (PEM-encoded). Required when CACertSecretRef
is set (unless DisableVerify is true).
type: string
caCertSecretRef:
description: |-
CACertSecretRef is a reference to a Kubernetes Secret containing
CA certificate(s) in PEM format. The Secret must be in the same
namespace as the resource referencing it (ModelConfig,
RemoteMCPServer, or any future consumer of TLSConfig).
When set, the certificate will be used to verify the upstream's
SSL certificate.
type: string
disableSystemCAs:
default: false
description: |-
DisableSystemCAs disables the use of system CA certificates.
When false (default), system CA certificates are used for verification (safe behavior).
When true, only the custom CA from CACertSecretRef is trusted.
This allows strict security policies where only corporate CAs should be trusted.
type: boolean
disableVerify:
default: false
description: |-
DisableVerify disables SSL certificate verification entirely.
When false (default), SSL certificates are verified.
When true, SSL certificate verification is disabled.
WARNING: This should ONLY be used in development/testing environments.
Production deployments MUST use proper certificates.
type: boolean
type: object
x-kubernetes-validations:
- message: caCertSecretKey requires caCertSecretRef
rule: '!(has(self.caCertSecretKey) && size(self.caCertSecretKey)
> 0 && (!has(self.caCertSecretRef) || size(self.caCertSecretRef)
== 0))'
- message: caCertSecretRef requires caCertSecretKey
rule: '!(has(self.caCertSecretRef) && size(self.caCertSecretRef)
> 0 && (!has(self.caCertSecretKey) || size(self.caCertSecretKey)
== 0))'
- message: disableSystemCAs requires caCertSecretRef or disableVerify
(trust-nothing config rejects every upstream)
rule: '!(has(self.disableSystemCAs) && self.disableSystemCAs &&
(!has(self.disableVerify) || !self.disableVerify) && (!has(self.caCertSecretRef)
|| size(self.caCertSecretRef) == 0))'
url:
minLength: 1
type: string
Expand Down Expand Up @@ -263,6 +321,12 @@ spec:
Important: Run "make" to regenerate code after modifying this file
format: int64
type: integer
secretHash:
description: |-
SecretHash stores a hash of the TLS Secret referenced by spec.tls so
agents that consume this RemoteMCPServer can detect cert rotation and
roll on the next reconcile. Empty when spec.tls.caCertSecretRef is unset.
type: string
type: object
type: object
served: true
Expand Down
40 changes: 28 additions & 12 deletions go/api/v1alpha2/modelconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,14 @@ type SAPAICoreConfig struct {
AuthURL string `json:"authUrl,omitempty"`
}

// TLSConfig contains TLS/SSL configuration options for model provider connections.
// This enables agents to connect to internal LiteLLM gateways or other providers
// that use self-signed certificates or custom certificate authorities.
// TLSConfig contains TLS/SSL configuration options for outbound HTTPS
// connections from the agent (model provider, RemoteMCPServer). The
// XValidation rules below apply at admission to every CRD field that
// uses TLSConfig, so callers don't need to re-declare them per spec.
//
// +kubebuilder:validation:XValidation:message="caCertSecretKey requires caCertSecretRef",rule="!(has(self.caCertSecretKey) && size(self.caCertSecretKey) > 0 && (!has(self.caCertSecretRef) || size(self.caCertSecretRef) == 0))"
// +kubebuilder:validation:XValidation:message="caCertSecretRef requires caCertSecretKey",rule="!(has(self.caCertSecretRef) && size(self.caCertSecretRef) > 0 && (!has(self.caCertSecretKey) || size(self.caCertSecretKey) == 0))"
// +kubebuilder:validation:XValidation:message="disableSystemCAs requires caCertSecretRef or disableVerify (trust-nothing config rejects every upstream)",rule="!(has(self.disableSystemCAs) && self.disableSystemCAs && (!has(self.disableVerify) || !self.disableVerify) && (!has(self.caCertSecretRef) || size(self.caCertSecretRef) == 0))"
Comment on lines +282 to +284
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.

if we have tests around this it would be good to validate that these changes are still consistent with the validation rules they replace in line 328-330

type TLSConfig struct {
// DisableVerify disables SSL certificate verification entirely.
// When false (default), SSL certificates are verified.
Expand All @@ -289,15 +294,16 @@ type TLSConfig struct {

// CACertSecretRef is a reference to a Kubernetes Secret containing
// CA certificate(s) in PEM format. The Secret must be in the same
// namespace as the ModelConfig.
// When set, the certificate will be used to verify the provider's SSL certificate.
// This field follows the same pattern as APIKeySecret.
// namespace as the resource referencing it (ModelConfig,
// RemoteMCPServer, or any future consumer of TLSConfig).
// When set, the certificate will be used to verify the upstream's
// SSL certificate.
// +optional
CACertSecretRef string `json:"caCertSecretRef,omitempty"`

// CACertSecretKey is the key within the Secret that contains the CA certificate data.
// This field follows the same pattern as APIKeySecretKey.
// Required when CACertSecretRef is set (unless DisableVerify is true).
// CACertSecretKey is the key within the Secret that contains the
// CA certificate data (PEM-encoded). Required when CACertSecretRef
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.

Why is this required, can't we infer a single key if there is one? I see it's required today but not enforced via cel, so I suppose this is just a cleaner UX given the current constraint.

// is set (unless DisableVerify is true).
// +optional
CACertSecretKey string `json:"caCertSecretKey,omitempty"`

Expand All @@ -310,6 +316,19 @@ type TLSConfig struct {
DisableSystemCAs bool `json:"disableSystemCAs,omitempty"`
}

// IsEmpty reports whether the TLSConfig carries any opinion. A nil
// receiver and an all-zero struct are equivalent — both mean "no TLS
// config supplied" and the consumer should fall back to its default
// behavior (typically system trust store, default httpx client). The
// single helper keeps callers from re-listing fields, so adding a new
// field to TLSConfig only requires updating this method.
func (t *TLSConfig) IsEmpty() bool {
if t == nil {
return true
}
return !t.DisableVerify && t.CACertSecretRef == "" && t.CACertSecretKey == "" && !t.DisableSystemCAs
}

// ModelConfigSpec defines the desired state of ModelConfig.
//
// +kubebuilder:validation:XValidation:message="provider.openAI must be nil if the provider is not OpenAI",rule="!(has(self.openAI) && self.provider != 'OpenAI')"
Expand All @@ -325,9 +344,6 @@ type TLSConfig struct {
// +kubebuilder:validation:XValidation:message="apiKeySecretKey must be set if apiKeySecret is set (except for Bedrock and SAPAICore providers)",rule="!(has(self.apiKeySecret) && !has(self.apiKeySecretKey) && self.provider != 'Bedrock' && self.provider != 'SAPAICore')"
// +kubebuilder:validation:XValidation:message="apiKeyPassthrough and apiKeySecret are mutually exclusive",rule="!(has(self.apiKeyPassthrough) && self.apiKeyPassthrough && has(self.apiKeySecret) && size(self.apiKeySecret) > 0)"
// +kubebuilder:validation:XValidation:message="apiKeyPassthrough must be false if provider is Gemini;GeminiVertexAI;AnthropicVertexAI",rule="!(has(self.apiKeyPassthrough) && self.apiKeyPassthrough && (self.provider == 'Gemini' || self.provider == 'GeminiVertexAI' || self.provider == 'AnthropicVertexAI'))"
// +kubebuilder:validation:XValidation:message="caCertSecretKey requires caCertSecretRef",rule="!(has(self.tls) && has(self.tls.caCertSecretKey) && size(self.tls.caCertSecretKey) > 0 && (!has(self.tls.caCertSecretRef) || size(self.tls.caCertSecretRef) == 0))"
// +kubebuilder:validation:XValidation:message="caCertSecretKey requires caCertSecretRef (unless disableVerify is true)",rule="!(has(self.tls) && (!has(self.tls.disableVerify) || !self.tls.disableVerify) && has(self.tls.caCertSecretKey) && size(self.tls.caCertSecretKey) > 0 && (!has(self.tls.caCertSecretRef) || size(self.tls.caCertSecretRef) == 0))"
// +kubebuilder:validation:XValidation:message="caCertSecretRef requires caCertSecretKey (unless disableVerify is true)",rule="!(has(self.tls) && (!has(self.tls.disableVerify) || !self.tls.disableVerify) && has(self.tls.caCertSecretRef) && size(self.tls.caCertSecretRef) > 0 && (!has(self.tls.caCertSecretKey) || size(self.tls.caCertSecretKey) == 0))"
// +kubebuilder:validation:XValidation:message="openAI.tokenExchange requires apiKeySecret (the service account secret)",rule="!(has(self.openAI) && has(self.openAI.tokenExchange) && (!has(self.apiKeySecret) || size(self.apiKeySecret) == 0))"
// +kubebuilder:validation:XValidation:message="openAI.tokenExchange and apiKeyPassthrough are mutually exclusive",rule="!(has(self.openAI) && has(self.openAI.tokenExchange) && has(self.apiKeyPassthrough) && self.apiKeyPassthrough)"
// +kubebuilder:validation:XValidation:message="openAI.tokenExchange type GDCHServiceAccount requires openAI.tokenExchange.gdchServiceAccount",rule="!(has(self.openAI) && has(self.openAI.tokenExchange) && self.openAI.tokenExchange.type == 'GDCHServiceAccount' && !has(self.openAI.tokenExchange.gdchServiceAccount))"
Expand Down
16 changes: 16 additions & 0 deletions go/api/v1alpha2/remotemcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ type RemoteMCPServerSpec struct {
// See: https://gateway-api.sigs.k8s.io/guides/multiple-ns/#cross-namespace-routing
// +optional
AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"`

// TLS configuration for the upstream MCP server connection.
// Use this for HTTPS upstreams that present a certificate the agent's
// system trust store does not include (corporate CA, self-signed cert
// on a test fixture, internal MCP gateway). Reuses the same TLSConfig
// type as ModelConfig.spec.tls, with identical semantics: disableVerify
// turns off certificate validation entirely, caCertSecretRef +
// caCertSecretKey point at a PEM bundle Secret in the same namespace,
// and disableSystemCAs trusts only the named bundle.
// +optional
TLS *TLSConfig `json:"tls,omitempty"`
}

var _ sql.Scanner = (*RemoteMCPServerSpec)(nil)
Expand Down Expand Up @@ -91,6 +102,11 @@ type RemoteMCPServerStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
// +optional
DiscoveredTools []*MCPTool `json:"discoveredTools,omitempty"`
// SecretHash stores a hash of the TLS Secret referenced by spec.tls so
// agents that consume this RemoteMCPServer can detect cert rotation and
// roll on the next reconcile. Empty when spec.tls.caCertSecretRef is unset.
// +optional
SecretHash string `json:"secretHash,omitempty"`
}

type MCPTool struct {
Expand Down
5 changes: 5 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func TestReconcileKagentMCPServer_ErrorPropagation(t *testing.T) {
types.NamespacedName{Namespace: "test", Name: "default-model"},
[]string{}, // No namespace restrictions for tests
nil,
nil,
)

// Call ReconcileKagentMCPServer
Expand Down
Loading
Loading