Skip to content

Commit cddfbff

Browse files
committed
Add TLS and NetworkPolicy for MCP sidecar service
Enable TLS on the MCP sidecar service using cert-manager with the internal CA issuer. When CaBundleSecretName is set (indicating TLS is active on the control plane), the controller provisions a TLS certificate for the MCP service DNS names via certmanager.EnsureCert(), mounts the cert secret into the MCP sidecar container at /etc/pki/tls/mcp/, and switches the service port from 8080 to 8443. The rhos-mcps config is updated to include TLS cert/key paths and use https for allowed origins. Add a Kubernetes NetworkPolicy that restricts ingress to the MCP port so that only pods labeled app.kubernetes.io/name=openstackassistant can connect, preventing unauthorized access to the MCP server. Changes: - internal/openstackclient/funcs.go: Add mcpTLSSecretName param to ClientPodSpec() for TLS secret volume mount; add tlsEnabled param to MCPConfigYAML() for TLS cert/key config and port selection - internal/controller/client/openstackclient_controller.go: Look up internal CA issuer by label, provision TLS cert, create NetworkPolicy, add RBAC markers for cert-manager.io and networking.k8s.io resources, register NetworkPolicy as owned resource - config/rbac/role.yaml, bindata/: Regenerated via make manifests and make bindata
1 parent 637694b commit cddfbff

4 files changed

Lines changed: 167 additions & 11 deletions

File tree

bindata/rbac/rbac.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,18 @@ rules:
695695
- get
696696
- list
697697
- watch
698+
- apiGroups:
699+
- networking.k8s.io
700+
resources:
701+
- networkpolicies
702+
verbs:
703+
- create
704+
- delete
705+
- get
706+
- list
707+
- patch
708+
- update
709+
- watch
698710
- apiGroups:
699711
- neutron.openstack.org
700712
resources:

config/rbac/role.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,18 @@ rules:
575575
- get
576576
- list
577577
- watch
578+
- apiGroups:
579+
- networking.k8s.io
580+
resources:
581+
- networkpolicies
582+
verbs:
583+
- create
584+
- delete
585+
- get
586+
- list
587+
- patch
588+
- update
589+
- watch
578590
- apiGroups:
579591
- neutron.openstack.org
580592
resources:

internal/controller/client/openstackclient_controller.go

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ import (
2323
"github.com/go-logr/logr"
2424

2525
corev1 "k8s.io/api/core/v1"
26+
networkingv1 "k8s.io/api/networking/v1"
2627
rbacv1 "k8s.io/api/rbac/v1"
2728
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/fields"
3031
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/apimachinery/pkg/util/intstr"
33+
"k8s.io/utils/ptr"
3134

3235
"k8s.io/apimachinery/pkg/runtime"
3336
"k8s.io/client-go/kubernetes"
@@ -41,7 +44,9 @@ import (
4144
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4245

4346
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
47+
"github.com/openstack-k8s-operators/lib-common/modules/certmanager"
4448
"github.com/openstack-k8s-operators/lib-common/modules/common"
49+
"github.com/openstack-k8s-operators/lib-common/modules/common/clusterdns"
4550
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
4651
"github.com/openstack-k8s-operators/lib-common/modules/common/configmap"
4752
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
@@ -83,6 +88,9 @@ func (r *OpenStackClientReconciler) GetLogger(ctx context.Context) logr.Logger {
8388
// +kubebuilder:rbac:groups="security.openshift.io",resourceNames=anyuid,resources=securitycontextconstraints,verbs=use
8489
// +kubebuilder:rbac:groups="",resources=pods,verbs=create;delete;get;list;patch;update;watch;patch
8590
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch
91+
// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
92+
// +kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch
93+
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete
8694

8795
// Reconcile -
8896
func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) {
@@ -308,7 +316,53 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
308316
instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage)
309317

310318
// Reconcile MCP sidecar resources when enabled
319+
mcpTLSSecretName := ""
311320
if instance.Spec.MCP != nil && instance.Spec.MCP.Enabled {
321+
mcpTLSEnabled := instance.Spec.CaBundleSecretName != ""
322+
323+
if mcpTLSEnabled {
324+
issuer, err := certmanager.GetIssuerByLabels(
325+
ctx, helper,
326+
instance.Namespace,
327+
map[string]string{certmanager.RootCAIssuerInternalLabel: ""},
328+
)
329+
if err != nil {
330+
instance.Status.Conditions.Set(condition.FalseCondition(
331+
clientv1.OpenStackClientReadyCondition,
332+
condition.ErrorReason,
333+
condition.SeverityWarning,
334+
clientv1.OpenStackClientReadyErrorMessage,
335+
err.Error()))
336+
return ctrl.Result{}, err
337+
}
338+
339+
clusterDomain := clusterdns.GetDNSClusterDomain()
340+
mcpSvcName := instance.Name + "-mcp"
341+
certRequest := certmanager.CertificateRequest{
342+
IssuerName: issuer.Name,
343+
CertName: mcpSvcName + "-tls",
344+
Hostnames: []string{
345+
fmt.Sprintf("%s.%s.svc", mcpSvcName, instance.Namespace),
346+
fmt.Sprintf("%s.%s.svc.%s", mcpSvcName, instance.Namespace, clusterDomain),
347+
},
348+
Labels: map[string]string{},
349+
}
350+
certSecret, ctrlResult, err := certmanager.EnsureCert(ctx, helper, certRequest, instance)
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+
return ctrlResult, nil
361+
}
362+
mcpTLSSecretName = certSecret.Name
363+
configVars[mcpTLSSecretName] = env.SetValue(certSecret.ResourceVersion)
364+
}
365+
312366
mcpConfigCM := &corev1.ConfigMap{
313367
ObjectMeta: metav1.ObjectMeta{
314368
Name: instance.Name + "-mcp-config",
@@ -317,14 +371,14 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
317371
}
318372
_, err = controllerutil.CreateOrPatch(ctx, r.Client, mcpConfigCM, func() error {
319373
mcpConfigCM.Data = map[string]string{
320-
"config.yaml": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName),
374+
"config.yaml": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName, mcpTLSEnabled),
321375
}
322376
return controllerutil.SetControllerReference(instance, mcpConfigCM, r.Scheme)
323377
})
324378
if err != nil {
325379
return ctrl.Result{}, fmt.Errorf("error creating MCP config ConfigMap: %w", err)
326380
}
327-
configVars[instance.Name+"-mcp-config"] = env.SetValue(openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName))
381+
configVars[instance.Name+"-mcp-config"] = env.SetValue(openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName, mcpTLSEnabled))
328382

329383
}
330384

@@ -335,6 +389,12 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
335389

336390
// Reconcile MCP Service after configVarsHash so the hash annotation captures all config changes
337391
if instance.Spec.MCP != nil && instance.Spec.MCP.Enabled {
392+
mcpTLSEnabled := instance.Spec.CaBundleSecretName != ""
393+
mcpPort := int32(8080)
394+
if mcpTLSEnabled {
395+
mcpPort = 8443
396+
}
397+
338398
mcpService := &corev1.Service{
339399
ObjectMeta: metav1.ObjectMeta{
340400
Name: instance.Name + "-mcp",
@@ -344,7 +404,7 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
344404
mcpServiceHash, err := util.ObjectHash(map[string]interface{}{
345405
"containerImage": instance.Spec.ContainerImage,
346406
"mcpContainerImage": instance.Spec.MCP.ContainerImage,
347-
"mcpConfig": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName),
407+
"mcpConfig": openstackclient.MCPConfigYAML(instance.Spec.CaBundleSecretName, mcpTLSEnabled),
348408
"configVarsHash": configVarsHash,
349409
})
350410
if err != nil {
@@ -359,7 +419,7 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
359419
mcpService.Spec.Ports = []corev1.ServicePort{
360420
{
361421
Name: "mcp",
362-
Port: 8080,
422+
Port: mcpPort,
363423
Protocol: corev1.ProtocolTCP,
364424
},
365425
}
@@ -368,6 +428,47 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
368428
if err != nil {
369429
return ctrl.Result{}, fmt.Errorf("error creating MCP Service: %w", err)
370430
}
431+
432+
// NetworkPolicy to restrict MCP access to openstackassistant pods only
433+
mcpNetPolicy := &networkingv1.NetworkPolicy{
434+
ObjectMeta: metav1.ObjectMeta{
435+
Name: instance.Name + "-mcp",
436+
Namespace: instance.Namespace,
437+
},
438+
}
439+
_, err = controllerutil.CreateOrPatch(ctx, r.Client, mcpNetPolicy, func() error {
440+
mcpNetPolicy.Spec = networkingv1.NetworkPolicySpec{
441+
PodSelector: metav1.LabelSelector{
442+
MatchLabels: clientLabels,
443+
},
444+
Ingress: []networkingv1.NetworkPolicyIngressRule{
445+
{
446+
From: []networkingv1.NetworkPolicyPeer{
447+
{
448+
PodSelector: &metav1.LabelSelector{
449+
MatchLabels: map[string]string{
450+
common.AppSelector: "openstackassistant",
451+
},
452+
},
453+
},
454+
},
455+
Ports: []networkingv1.NetworkPolicyPort{
456+
{
457+
Port: &intstr.IntOrString{Type: intstr.Int, IntVal: mcpPort},
458+
Protocol: ptr.To(corev1.ProtocolTCP),
459+
},
460+
},
461+
},
462+
},
463+
PolicyTypes: []networkingv1.PolicyType{
464+
networkingv1.PolicyTypeIngress,
465+
},
466+
}
467+
return controllerutil.SetControllerReference(instance, mcpNetPolicy, r.Scheme)
468+
})
469+
if err != nil {
470+
return ctrl.Result{}, fmt.Errorf("error creating MCP NetworkPolicy: %w", err)
471+
}
371472
}
372473

373474
osclient := &corev1.Pod{
@@ -377,7 +478,7 @@ func (r *OpenStackClientReconciler) Reconcile(ctx context.Context, req ctrl.Requ
377478
},
378479
}
379480

380-
spec := openstackclient.ClientPodSpec(ctx, instance, helper, configVarsHash)
481+
spec := openstackclient.ClientPodSpec(ctx, instance, helper, configVarsHash, mcpTLSSecretName)
381482

382483
podSpecHash, err := util.ObjectHash(spec)
383484
if err != nil {
@@ -571,6 +672,7 @@ func (r *OpenStackClientReconciler) SetupWithManager(
571672
Owns(&corev1.ServiceAccount{}).
572673
Owns(&corev1.ConfigMap{}).
573674
Owns(&corev1.Service{}).
675+
Owns(&networkingv1.NetworkPolicy{}).
574676
Owns(&rbacv1.Role{}).
575677
Owns(&rbacv1.RoleBinding{}).
576678
Watches(

internal/openstackclient/funcs.go

Lines changed: 36 additions & 6 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+
mcpTLSSecretName string,
3738
) corev1.PodSpec {
3839
envVars := map[string]env.Setter{}
3940
envVars["OS_CLOUD"] = env.SetValue("default")
@@ -135,6 +136,24 @@ func ClientPodSpec(
135136
mcpVolumeMounts = append(mcpVolumeMounts, instance.Spec.CreateVolumeMounts(nil)...)
136137
}
137138

139+
mcpPort := int32(8080)
140+
if mcpTLSSecretName != "" {
141+
mcpPort = 8443
142+
mcpVolumeMounts = append(mcpVolumeMounts, corev1.VolumeMount{
143+
Name: "mcp-tls",
144+
MountPath: "/etc/pki/tls/mcp",
145+
ReadOnly: true,
146+
})
147+
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
148+
Name: "mcp-tls",
149+
VolumeSource: corev1.VolumeSource{
150+
Secret: &corev1.SecretVolumeSource{
151+
SecretName: mcpTLSSecretName,
152+
},
153+
},
154+
})
155+
}
156+
138157
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
139158
Name: "mcp-config",
140159
VolumeSource: corev1.VolumeSource{
@@ -156,7 +175,7 @@ func ClientPodSpec(
156175
{Name: "OS_CLIENT_CONFIG_FILE", Value: "/home/cloud-admin/.config/openstack/clouds.yaml"},
157176
},
158177
Ports: []corev1.ContainerPort{
159-
{Name: "mcp", ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
178+
{Name: "mcp", ContainerPort: mcpPort, Protocol: corev1.ProtocolTCP},
160179
},
161180
SecurityContext: &corev1.SecurityContext{
162181
RunAsUser: ptr.To[int64](42401),
@@ -179,24 +198,35 @@ func ClientPodSpec(
179198
}
180199

181200
// MCPConfigYAML returns the rhos-mcps config.yaml content for the MCP sidecar
182-
func MCPConfigYAML(caBundleSecretName string) string {
201+
func MCPConfigYAML(caBundleSecretName string, tlsEnabled bool) string {
183202
caCert := ""
184203
if caBundleSecretName != "" {
185204
caCert = fmt.Sprintf("\n ca_cert: %s", tls.DownstreamTLSCABundlePath)
186205
}
187-
return fmt.Sprintf(`port: 8080
206+
port := "8080"
207+
tlsConfig := ""
208+
allowedOriginScheme := "http"
209+
if tlsEnabled {
210+
port = "8443"
211+
tlsConfig = `
212+
tls:
213+
cert_file: /etc/pki/tls/mcp/tls.crt
214+
key_file: /etc/pki/tls/mcp/tls.key`
215+
allowedOriginScheme = "https"
216+
}
217+
return fmt.Sprintf(`port: %s
188218
openstack:
189219
enabled: true
190220
allow_write: false%s
191221
openshift:
192-
enabled: false
222+
enabled: false%s
193223
mcp_transport_security:
194224
enable_dns_rebinding_protection: false
195225
allowed_hosts:
196226
- "*:*"
197227
allowed_origins:
198-
- "http://*:*"
199-
`, caCert)
228+
- "%s://*:*"
229+
`, port, caCert, tlsConfig, allowedOriginScheme)
200230
}
201231

202232
func clientPodVolumeMounts() []corev1.VolumeMount {

0 commit comments

Comments
 (0)