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
4 changes: 4 additions & 0 deletions go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ setup-envtest: envtest ## Download the binaries required for ENVTEST in the loca
exit 1; \
}

.PHONY: envtest-path
envtest-path: envtest ## Print the path to the envtest binaries (downloads them if absent).
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
Expand Down
44 changes: 22 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,19 @@ 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 — admission rejects ref-without-key regardless of
DisableVerify (see the TLSConfig-level XValidation rules).
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 +684,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 +735,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
75 changes: 75 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,13 +179,82 @@ 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 — 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.

Note one asymmetry with ModelConfig: a spec-level XValidation rule
on RemoteMCPServer rejects spec.tls when spec.url has the http://
scheme (a TLS opinion contradicts a plaintext URL). ModelConfig has
no equivalent rule, so a TLS block can sit alongside any baseUrl.
properties:
caCertSecretKey:
description: |-
CACertSecretKey is the key within the Secret that contains the
CA certificate data (PEM-encoded). Required when CACertSecretRef
is set — admission rejects ref-without-key regardless of
DisableVerify (see the TLSConfig-level XValidation rules).
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
required:
- description
- url
type: object
x-kubernetes-validations:
- message: 'spec.tls must be unset when spec.url has http:// scheme: a
TLS opinion contradicts a plaintext URL. Either drop spec.tls, or
use https:// / a scheme-less URL.'
rule: '!self.url.startsWith(''http://'') || !has(self.tls)'
status:
description: RemoteMCPServerStatus defines the observed state of RemoteMCPServer.
properties:
Expand Down Expand Up @@ -263,6 +332,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
41 changes: 29 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 thread
iplay88keys marked this conversation as resolved.
type TLSConfig struct {
// DisableVerify disables SSL certificate verification entirely.
// When false (default), SSL certificates are verified.
Expand All @@ -289,15 +294,17 @@ 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.

Copy link
Copy Markdown
Contributor Author

@iplay88keys iplay88keys May 28, 2026

Choose a reason for hiding this comment

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

We can definitely infer if there is only one key. This just keeps with the existing modelconfig approach for now.

Also, this is more extensible. We can make sure that there's a default and it's not required, but I think it's just beneficial to have the ability to specify it here. For example, cert-manager-generated TLS secrets have tls.crt, tls.key, and ca.crt, so there is precedent for a secret with multiple keys and the right one would need to be fetched.

// is set — admission rejects ref-without-key regardless of
// DisableVerify (see the TLSConfig-level XValidation rules).
// +optional
CACertSecretKey string `json:"caCertSecretKey,omitempty"`

Expand All @@ -310,6 +317,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 +345,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
23 changes: 23 additions & 0 deletions go/api/v1alpha2/remotemcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const (
)

// RemoteMCPServerSpec defines the desired state of RemoteMCPServer.
//
// +kubebuilder:validation:XValidation:message="spec.tls must be unset when spec.url has http:// scheme: a TLS opinion contradicts a plaintext URL. Either drop spec.tls, or use https:// / a scheme-less URL.",rule="!self.url.startsWith('http://') || !has(self.tls)"
type RemoteMCPServerSpec struct {
// +required
Description string `json:"description"`
Expand All @@ -63,6 +65,22 @@ 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 — 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.
//
// Note one asymmetry with ModelConfig: a spec-level XValidation rule
// on RemoteMCPServer rejects spec.tls when spec.url has the http://
// scheme (a TLS opinion contradicts a plaintext URL). ModelConfig has
// no equivalent rule, so a TLS block can sit alongside any baseUrl.
// +optional
TLS *TLSConfig `json:"tls,omitempty"`
}

var _ sql.Scanner = (*RemoteMCPServerSpec)(nil)
Expand Down Expand Up @@ -91,6 +109,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
Loading
Loading