Skip to content

Commit 62ce7ee

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 62ce7ee

5 files changed

Lines changed: 125 additions & 15 deletions

File tree

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: 50 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,6 +314,7 @@ 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 {
316319
// Use the internal Keystone endpoint for the MCP sidecar's clouds.yaml
317320
// so it connects directly to the in-cluster service and avoids
@@ -326,6 +329,47 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
326329
return ctrl.Result{RequeueAfter: time.Duration(5) * time.Second}, nil
327330
}
328331

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

355399
}
356400

@@ -403,7 +447,7 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
403447
},
404448
}
405449

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

408452
podSpecHash, err := util.ObjectHash(spec)
409453
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

internal/openstackclient/funcs.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func ClientPodSpec(
3434
instance *clientv1.OpenStackClient,
3535
helper *helper.Helper,
3636
configHash string,
37+
mcpTLSCertSecret string,
3738
) corev1.PodSpec {
3839
envVars := map[string]env.Setter{}
3940
envVars["OS_CLOUD"] = env.SetValue("default")
@@ -137,6 +138,23 @@ func ClientPodSpec(
137138
mcpVolumeMounts = append(mcpVolumeMounts, instance.Spec.CreateVolumeMounts(nil)...)
138139
}
139140

141+
if mcpTLSCertSecret != "" {
142+
mcpVolumeMounts = append(mcpVolumeMounts,
143+
corev1.VolumeMount{
144+
Name: "mcp-tls-cert",
145+
MountPath: "/etc/pki/tls/certs/tls.crt",
146+
SubPath: "tls.crt",
147+
ReadOnly: true,
148+
},
149+
corev1.VolumeMount{
150+
Name: "mcp-tls-cert",
151+
MountPath: "/etc/pki/tls/private/tls.key",
152+
SubPath: "tls.key",
153+
ReadOnly: true,
154+
},
155+
)
156+
}
157+
140158
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
141159
Name: "mcp-config",
142160
VolumeSource: corev1.VolumeSource{
@@ -148,6 +166,18 @@ func ClientPodSpec(
148166
},
149167
})
150168

169+
if mcpTLSCertSecret != "" {
170+
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
171+
Name: "mcp-tls-cert",
172+
VolumeSource: corev1.VolumeSource{
173+
Secret: &corev1.SecretVolumeSource{
174+
SecretName: mcpTLSCertSecret,
175+
DefaultMode: ptr.To[int32](0440),
176+
},
177+
},
178+
})
179+
}
180+
151181
mcpEnvVars := []corev1.EnvVar{
152182
{Name: "HOME", Value: "/home/cloud-admin"},
153183
{Name: "RHOS_MCPS_CONFIG", Value: "/tmp/mcp-config/config.yaml"},
@@ -190,11 +220,23 @@ func ClientPodSpec(
190220
}
191221

192222
// MCPConfigYAML returns the rhos-mcps config.yaml content for the MCP sidecar
193-
func MCPConfigYAML(caBundleSecretName string) string {
223+
func MCPConfigYAML(caBundleSecretName string, mcpTLSEnabled bool) string {
194224
caCert := ""
195225
if caBundleSecretName != "" {
196226
caCert = fmt.Sprintf("\n ca_cert: %s", tls.DownstreamTLSCABundlePath)
197227
}
228+
mcpTLS := ""
229+
if mcpTLSEnabled {
230+
mcpTLS = `
231+
tls:
232+
ssl_certfile: /etc/pki/tls/certs/tls.crt
233+
ssl_keyfile: /etc/pki/tls/private/tls.key`
234+
}
235+
allowedOrigins := ` - "http://*:*"`
236+
if mcpTLSEnabled {
237+
allowedOrigins = ` - "http://*:*"
238+
- "https://*:*"`
239+
}
198240
return fmt.Sprintf(`port: 8080
199241
openstack:
200242
enabled: true
@@ -206,8 +248,8 @@ mcp_transport_security:
206248
allowed_hosts:
207249
- "*:*"
208250
allowed_origins:
209-
- "http://*:*"
210-
`, caCert)
251+
%s%s
252+
`, caCert, allowedOrigins, mcpTLS)
211253
}
212254

213255
// MCPCloudsYAML returns a clouds.yaml using the given auth URL for the MCP sidecar.

0 commit comments

Comments
 (0)