Skip to content

Commit 4f28dee

Browse files
committed
Add podLevel TLS between OpenStackAssistant and MCP server sidecar
Adds TLS encryption for the Streamable HTTP connection between the OpenStackAssistant pod (Goose) and the rhos-ls-mcps MCP server sidecar running in the OpenStackClient pod. TLS is conditional: enabled when CaBundleSecretName is set on the OpenStackClient (indicating TLS is active in the cluster via the OpenStackControlPlane). When enabled, the OpenStackClient controller creates a cert-manager Certificate using the existing rootca-internal issuer for the MCP service endpoint. The resulting TLS cert/key are mounted into the MCP sidecar container and referenced in the rhos-mcps config.yaml via the new tls.ssl_certfile/ssl_keyfile fields. The assistant controller switches resolved MCP URLs from http:// to https:// when the referenced OpenStackClient has TLS enabled, and the existing combined-ca-bundle mount provides the internal CA for verification. The existing TLS between the MCP server sidecar and OpenStack services (Keystone etc. via OS_CACERT/REQUESTS_CA_BUNDLE) is unchanged. Also fixes the mcp-ca-bundle SubPath in the assistant pod from "ca-bundle.crt" to "tls-ca-bundle.pem" to match the actual key in the combined-ca-bundle secret. Files changed: - internal/controller/client/openstackclient_controller.go - internal/openstackclient/funcs.go - internal/controller/assistant/openstackassistant_controller.go - internal/openstackassistant/funcs.go - internal/openstackassistant/funcs_test.go
1 parent d74b21e commit 4f28dee

11 files changed

Lines changed: 144 additions & 24 deletions

File tree

api/assistant/v1beta1/openstackassistant_types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ type GooseConfig struct {
9898

9999
// OpenStackAssistantSpec defines the desired state of OpenStackAssistant
100100
type OpenStackAssistantSpec struct {
101-
// ContainerImage for the assistant container.
102-
// +kubebuilder:validation:Required
103-
ContainerImage string `json:"containerImage"`
101+
// ContainerImage for the assistant container (will be set to environmental default if empty).
102+
// +kubebuilder:validation:Optional
103+
ContainerImage string `json:"containerImage,omitempty"`
104104

105105
// Provider is the AI agent provider type. Currently only "goose" is supported.
106106
// +kubebuilder:validation:Optional

api/bases/assistant.openstack.org_openstackassistants.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ spec:
5353
description: OpenStackAssistantSpec defines the desired state of OpenStackAssistant
5454
properties:
5555
containerImage:
56-
description: ContainerImage for the assistant container.
56+
description: ContainerImage for the assistant container (will be set
57+
to environmental default if empty).
5758
type: string
5859
env:
5960
description: Env is a list of additional environment variables for
@@ -259,7 +260,6 @@ spec:
259260
- goose
260261
type: string
261262
required:
262-
- containerImage
263263
- lightspeedStack
264264
type: object
265265
status:

bindata/crds/crds.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ spec:
5252
description: OpenStackAssistantSpec defines the desired state of OpenStackAssistant
5353
properties:
5454
containerImage:
55-
description: ContainerImage for the assistant container.
55+
description: ContainerImage for the assistant container (will be set
56+
to environmental default if empty).
5657
type: string
5758
env:
5859
description: Env is a list of additional environment variables for
@@ -258,7 +259,6 @@ spec:
258259
- goose
259260
type: string
260261
required:
261-
- containerImage
262262
- lightspeedStack
263263
type: object
264264
status:

config/crd/bases/assistant.openstack.org_openstackassistants.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ spec:
5353
description: OpenStackAssistantSpec defines the desired state of OpenStackAssistant
5454
properties:
5555
containerImage:
56-
description: ContainerImage for the assistant container.
56+
description: ContainerImage for the assistant container (will be set
57+
to environmental default if empty).
5758
type: string
5859
env:
5960
description: Env is a list of additional environment variables for
@@ -259,7 +260,6 @@ spec:
259260
- goose
260261
type: string
261262
required:
262-
- containerImage
263263
- lightspeedStack
264264
type: object
265265
status:

config/manifests/bases/openstack-operator.clusterserviceversion.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ spec:
2323
apiservicedefinitions: {}
2424
customresourcedefinitions:
2525
owned:
26+
- description: OpenStackAssistant is the Schema for the openstackassistants API
27+
displayName: OpenStack Assistant
28+
kind: OpenStackAssistant
29+
name: openstackassistants.assistant.openstack.org
30+
version: v1beta1
2631
- description: |-
2732
OpenStackBackupConfig is the Schema for the openstackbackupconfigs API.
2833
It configures automatic backup labeling for user-provided resources (without ownerReferences).

config/operator/default_images.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ spec:
159159
value: quay.io/podified-antelope-centos9/openstack-rsyslog:current-podified
160160
- name: RELATED_IMAGE_OPENSTACK_CLIENT_IMAGE_URL_DEFAULT
161161
value: quay.io/podified-antelope-centos9/openstack-openstackclient:current-podified
162+
- name: RELATED_IMAGE_OPENSTACK_MCP_IMAGE_URL_DEFAULT
163+
value: quay.io/dprince/rhos-mcps:latest
162164
- name: RELATED_IMAGE_OS_CONTAINER_IMAGE_URL_DEFAULT
163165
value: quay.io/podified-antelope-centos9/edpm-hardened-uefi:current-podified
164166
- name: RELATED_IMAGE_OVN_CONTROLLER_IMAGE_URL_DEFAULT

internal/controller/assistant/openstackassistant_controller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,11 @@ func (r *OpenStackAssistantReconciler) Reconcile(ctx context.Context, req ctrl.R
327327
if osclient.Spec.CaBundleSecretName != "" {
328328
mcpCaBundleSecretName = osclient.Spec.CaBundleSecretName
329329
}
330-
mcpURL := fmt.Sprintf("http://%s.%s.svc:8080/openstack/", mcpSvcName, instance.Namespace)
330+
scheme := "http"
331+
if osclient.Spec.CaBundleSecretName != "" {
332+
scheme = "https"
333+
}
334+
mcpURL := fmt.Sprintf("%s://%s.%s.svc:8080/openstack/", scheme, mcpSvcName, instance.Namespace)
331335
resolvedMCPServers[mcp.Name] = mcpURL
332336
Log.Info("Auto-resolved MCP server", "name", mcp.Name, "url", mcpURL, "openstackClientRef", mcp.OpenStackClientRef)
333337
} else if mcp.URL != "" {

internal/controller/client/openstackclient_controller.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,21 @@ import (
4040
"sigs.k8s.io/controller-runtime/pkg/predicate"
4141
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4242

43+
certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
4344
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
45+
"github.com/openstack-k8s-operators/lib-common/modules/certmanager"
4446
"github.com/openstack-k8s-operators/lib-common/modules/common"
4547
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
4648
"github.com/openstack-k8s-operators/lib-common/modules/common/configmap"
4749
"github.com/openstack-k8s-operators/lib-common/modules/common/endpoint"
4850
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
4951
helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
5052
common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
51-
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
52-
telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1"
53-
5453
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
54+
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
55+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
5556
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
57+
telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1"
5658
clientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1"
5759
"github.com/openstack-k8s-operators/openstack-operator/internal/openstackclient"
5860
)
@@ -312,7 +314,11 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
312314
instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage)
313315

314316
// Reconcile MCP sidecar resources when enabled
317+
mcpTLSCertSecret := ""
315318
if instance.Spec.MCP != nil && instance.Spec.MCP.Enabled {
319+
if instance.Spec.MCPContainerImage == "" {
320+
return ctrl.Result{}, fmt.Errorf("MCP is enabled but MCPContainerImage is not set")
321+
}
316322
// Use the internal Keystone endpoint for the MCP sidecar's clouds.yaml
317323
// so it connects directly to the in-cluster service and avoids
318324
// TLS issues with the public OCP route.
@@ -326,6 +332,47 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
326332
return ctrl.Result{RequeueAfter: time.Duration(5) * time.Second}, nil
327333
}
328334

335+
// Create TLS certificate for MCP server when TLS is enabled
336+
if instance.Spec.CaBundleSecretName != "" {
337+
issuerName := tls.DefaultCAPrefix + string(service.EndpointInternal)
338+
mcpSvcName := instance.Name + "-mcp"
339+
certRequest := certmanager.CertificateRequest{
340+
IssuerName: issuerName,
341+
CertName: mcpSvcName + "-tls",
342+
Hostnames: []string{
343+
fmt.Sprintf("%s.%s.svc", mcpSvcName, instance.Namespace),
344+
fmt.Sprintf("%s.%s.svc.cluster.local", mcpSvcName, instance.Namespace),
345+
},
346+
Usages: []certmgrv1.KeyUsage{
347+
certmgrv1.UsageKeyEncipherment,
348+
certmgrv1.UsageDigitalSignature,
349+
certmgrv1.UsageServerAuth,
350+
},
351+
Labels: map[string]string{},
352+
}
353+
certSecret, ctrlResult, err := certmanager.EnsureCert(ctx, helper, certRequest, nil)
354+
if err != nil {
355+
instance.Status.Conditions.Set(condition.FalseCondition(
356+
clientv1.OpenStackClientReadyCondition,
357+
condition.ErrorReason,
358+
condition.SeverityWarning,
359+
clientv1.OpenStackClientReadyErrorMessage,
360+
err.Error()))
361+
return ctrlResult, err
362+
} else if (ctrlResult != ctrl.Result{}) {
363+
instance.Status.Conditions.Set(condition.FalseCondition(
364+
clientv1.OpenStackClientReadyCondition,
365+
condition.RequestedReason,
366+
condition.SeverityInfo,
367+
clientv1.OpenStackClientReadyRunningMessage))
368+
return ctrlResult, nil
369+
}
370+
mcpTLSCertSecret = certSecret.Name
371+
configVars[mcpTLSCertSecret] = env.SetValue(mcpTLSCertSecret)
372+
}
373+
374+
mcpTLSEnabled := mcpTLSCertSecret != ""
375+
329376
mcpCloudsYAML := openstackclient.MCPCloudsYAML(
330377
internalAuthURL,
331378
keystoneAPI.Spec.AdminProject,
@@ -342,15 +389,15 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
342389
}
343390
_, err = controllerutil.CreateOrPatch(ctx, r.Client, mcpConfigCM, func() error {
344391
mcpConfigCM.Data = map[string]string{
345-
"config.yaml": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName),
392+
"config.yaml": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName, mcpTLSEnabled),
346393
"clouds.yaml": mcpCloudsYAML,
347394
}
348395
return controllerutil.SetControllerReference(instance, mcpConfigCM, r.Scheme)
349396
})
350397
if err != nil {
351398
return ctrl.Result{}, fmt.Errorf("error creating MCP config ConfigMap: %w", err)
352399
}
353-
configVars[instance.Name+"-mcp-config"] = env.SetValue(openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName) + mcpCloudsYAML)
400+
configVars[instance.Name+"-mcp-config"] = env.SetValue(openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName, mcpTLSEnabled) + mcpCloudsYAML)
354401

355402
}
356403

@@ -403,7 +450,7 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
403450
},
404451
}
405452

406-
spec := openstackclient.ClientPodSpec(ctx, instance, helper, configVarsHash)
453+
spec := openstackclient.ClientPodSpec(ctx, instance, helper, configVarsHash, mcpTLSCertSecret)
407454

408455
podSpecHash, err := util.ObjectHash(spec)
409456
if err != nil {

internal/openstackassistant/funcs.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package openstackassistant
1515

1616
import (
1717
env "github.com/openstack-k8s-operators/lib-common/modules/common/env"
18+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
1819
assistantv1 "github.com/openstack-k8s-operators/openstack-operator/api/assistant/v1beta1"
1920

2021
corev1 "k8s.io/api/core/v1"
@@ -105,9 +106,9 @@ fi
105106
# Combine CA bundles if MCP CA is present alongside Lightspeed CA
106107
if [ -f /etc/ssl/certs/mcp-ca-bundle.crt ]; then
107108
if [ -f /etc/ssl/certs/ca-certificates.crt ]; then
108-
cp /etc/ssl/certs/ca-certificates.crt /tmp/combined-ca.crt
109-
cat /etc/ssl/certs/mcp-ca-bundle.crt >> /tmp/combined-ca.crt
110-
export SSL_CERT_FILE=/tmp/combined-ca.crt
109+
cp /etc/ssl/certs/ca-certificates.crt $HOME/combined-ca.crt
110+
cat /etc/ssl/certs/mcp-ca-bundle.crt >> $HOME/combined-ca.crt
111+
export SSL_CERT_FILE=$HOME/combined-ca.crt
111112
else
112113
export SSL_CERT_FILE=/etc/ssl/certs/mcp-ca-bundle.crt
113114
fi
@@ -240,7 +241,7 @@ func assistantPodVolumeMounts(instance *assistantv1.OpenStackAssistant, mcpCaBun
240241
mounts = append(mounts, corev1.VolumeMount{
241242
Name: "mcp-ca-bundle",
242243
MountPath: "/etc/ssl/certs/mcp-ca-bundle.crt",
243-
SubPath: "ca-bundle.crt",
244+
SubPath: tls.CABundleKey,
244245
ReadOnly: true,
245246
})
246247
}

internal/openstackassistant/funcs_test.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,25 @@ func TestAssistantPodSpec_MCPServers(t *testing.T) {
333333
g.Expect(envMap).To(HaveKeyWithValue("MCP_SERVER_openstack", "http://openstackclient-mcp.openstack.svc:8080/openstack/"))
334334
}
335335

336+
func TestAssistantPodSpec_MCPServersHTTPS(t *testing.T) {
337+
g := NewWithT(t)
338+
instance := newTestInstance()
339+
340+
resolvedMCPServers := map[string]string{
341+
"openstack": "https://openstackclient-mcp.openstack.svc:8080/openstack/",
342+
}
343+
344+
spec := AssistantPodSpec(instance, "hash", resolvedMCPServers, "")
345+
envVars := spec.Containers[0].Env
346+
347+
envMap := make(map[string]string)
348+
for _, e := range envVars {
349+
envMap[e.Name] = e.Value
350+
}
351+
352+
g.Expect(envMap).To(HaveKeyWithValue("MCP_SERVER_openstack", "https://openstackclient-mcp.openstack.svc:8080/openstack/"))
353+
}
354+
336355
func TestAssistantPodSpec_MCPCaBundle(t *testing.T) {
337356
g := NewWithT(t)
338357
instance := newTestInstance()
@@ -358,7 +377,7 @@ func TestAssistantPodSpec_MCPCaBundle(t *testing.T) {
358377
}
359378
g.Expect(mcpCaMount).NotTo(BeNil())
360379
g.Expect(mcpCaMount.MountPath).To(Equal("/etc/ssl/certs/mcp-ca-bundle.crt"))
361-
g.Expect(mcpCaMount.SubPath).To(Equal("ca-bundle.crt"))
380+
g.Expect(mcpCaMount.SubPath).To(Equal("tls-ca-bundle.pem"))
362381
g.Expect(mcpCaMount.ReadOnly).To(BeTrue())
363382
}
364383

0 commit comments

Comments
 (0)