Skip to content

Commit 283396d

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 283396d

6 files changed

Lines changed: 130 additions & 15 deletions

File tree

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/podified-antelope-centos9/openstack-rhos-mcps:current-podified
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

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)