Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: add TLS support in the operator metrics port
### Is there an existing issue already for this feature request/idea?

- [X] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.

### What problem is this feature going to solve? Why should it be added?

As part of increased security for CloudNativePG, the prometheus metrics port
served in port 9187 should support TLS

### Describe the solution you'd like

We should enable a new CLI option for the manager to enable TLS. Defaulting to false.
Possibly add a new mount for the certificates, similar to what we have for the webhook certificates

I.e. in `/config/manager/manager.yaml`
```
- mountPath: /run/secrets/cnpg.io/metrics
  name: metrics-certificates
```



### Describe alternatives you've considered

We could use an environment variable for the option.

### Additional context

_No response_

### Backport?

Yes

### Are you willing to actively contribute to this feature?

Yes

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Discussion:
@github-actions: This issue is stale because it has been open for 60 days with no activity.@github-actions: This issue was closed because it has been inactive for 14 days since being marked as stale.@narimonf: I would be interested in this

Solution patch:
diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt
index de315c42..00baf8fc 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -83,6 +83,7 @@ ClusterIP
 ClusterImageCatalog
 ClusterIsNotReady
 ClusterList
+ClusterMonitoringTLSConfiguration
 ClusterRole
 ClusterRole's
 ClusterServiceVersion
@@ -702,6 +703,7 @@ ecdsa
 edb
 eks
 enableAlterSystem
+enableMetricsTLS
 enablePDB
 enablePodAntiAffinity
 enablePodMonitor
diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go
index a86b0784..5c4ce84e 100644
--- a/api/v1/cluster_types.go
+++ b/api/v1/cluster_types.go
@@ -2403,6 +2403,11 @@ type MonitoringConfiguration struct {
 	// +optional
 	EnablePodMonitor bool `json:"enablePodMonitor,omitempty"`
 
+	// Configure TLS communication for the metrics endpoint.
+	// Changing tls.enabled option will force a rollout of all instances.
+	// +optional
+	TLSConfig *ClusterMonitoringTLSConfiguration `json:"tls,omitempty"`
+
 	// The list of metric relabelings for the `PodMonitor`. Applied to samples before ingestion.
 	// +optional
 	PodMonitorMetricRelabelConfigs []monitoringv1.RelabelConfig `json:"podMonitorMetricRelabelings,omitempty"`
@@ -2417,6 +2422,16 @@ func (m *MonitoringConfiguration) AreDefaultQueriesDisabled() bool {
 	return m != nil && m.DisableDefaultQueries != nil && *m.DisableDefaultQueries
 }
 
+// ClusterMonitoringTLSConfiguration is the type containing the TLS configuration
+// for the cluster's monitoring
+type ClusterMonitoringTLSConfiguration struct {
+	// Enable TLS for the monitoring endpoint.
+	// Changing this option will force a rollout of all instances.
+	// +kubebuilder:default:=false
+	// +optional
+	Enabled bool `json:"enabled,omitempty"`
+}
+
 // ExternalCluster represents the connection parameters to an
 // external cluster which is used in the other sections of the configuration
 type ExternalCluster struct {
@@ -3588,6 +3603,15 @@ func (cluster *Cluster) IsPodMonitorEnabled() bool {
 	return false
 }
 
+// IsMetricsTLSEnabled checks if the metrics endpoint should use TLS
+func (cluster *Cluster) IsMetricsTLSEnabled() bool {
+	if cluster.Spec.Monitoring != nil && cluster.Spec.Monitoring.TLSConfig != nil {
+		return cluster.Spec.Monitoring.TLSConfig.Enabled
+	}
+
+	return false
+}
+
 // GetEnableSuperuserAccess returns if the superuser access is enabled or not
 func (cluster *Cluster) GetEnableSuperuserAccess() bool {
 	if cluster.Spec.EnableSuperuserAccess != nil {
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index 581b262a..04201dfc 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -771,6 +771,21 @@ func (in *ClusterList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterMonitoringTLSConfiguration) DeepCopyInto(out *ClusterMonitoringTLSConfiguration) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterMonitoringTLSConfiguration.
+func (in *ClusterMonitoringTLSConfiguration) DeepCopy() *ClusterMonitoringTLSConfiguration {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterMonitoringTLSConfiguration)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) {
 	*out = *in
@@ -1697,6 +1712,11 @@ func (in *MonitoringConfiguration) DeepCopyInto(out *MonitoringConfiguration) {
 		*out = make([]SecretKeySelector, len(*in))
 		copy(*out, *in)
 	}
+	if in.TLSConfig != nil {
+		in, out := &in.TLSConfig, &out.TLSConfig
+		*out = new(ClusterMonitoringTLSConfiguration)
+		**out = **in
+	}
 	if in.PodMonitorMetricRelabelConfigs != nil {
 		in, out := &in.PodMonitorMetricRelabelConfigs, &out.PodMonitorMetricRelabelConfigs
 		*out = make([]monitoringv1.RelabelConfig, len(*in))
diff --git a/config/crd/bases/postgresql.cnpg.io_clusters.yaml b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
index a2ee2b5a..fa1c2126 100644
--- a/config/crd/bases/postgresql.cnpg.io_clusters.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
@@ -3909,6 +3909,18 @@ spec:
                           type: string
                       type: object
                     type: array
+                  tls:
+                    description: |-
+                      Configure TLS communication for the metrics endpoint.
+                      Changing tls.enabled option will force a rollout of all instances.
+                    properties:
+                      enabled:
+                        default: false
+                        description: |-
+                          Enable TLS for the monitoring endpoint.
+                          Changing this option will force a rollout of all instances.
+                        type: boolean
+                    type: object
                 type: object
               nodeMaintenanceWindow:
                 description: Define a maintenance window for the Kubernetes nodes
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index dee405a6..cb162b2d 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -1535,6 +1535,32 @@ this can be omitted.<!-- raw HTML omitted --></li>
 </tbody>
 </table>
 
+## ClusterMonitoringTLSConfiguration     {#postgresql-cnpg-io-v1-ClusterMonitoringTLSConfiguration}
+
+
+**Appears in:**
+
+- [MonitoringConfiguration](#postgresql-cnpg-io-v1-MonitoringConfiguration)
+
+
+<p>ClusterMonitoringTLSConfiguration is the type containing the TLS configuration
+for the cluster's monitoring</p>
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+<tr><td><code>enabled</code><br/>
+<i>bool</i>
+</td>
+<td>
+   <p>Enable TLS for the monitoring endpoint.
+Changing this option will force a rollout of all instances.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## ClusterSpec     {#postgresql-cnpg-io-v1-ClusterSpec}
 
 
@@ -3366,6 +3392,14 @@ Default: false.</p>
    <p>Enable or disable the <code>PodMonitor</code></p>
 </td>
 </tr>
+<tr><td><code>tls</code><br/>
+<a href="#postgresql-cnpg-io-v1-ClusterMonitoringTLSConfiguration"><i>ClusterMonitoringTLSConfiguration</i></a>
+</td>
+<td>
+   <p>Configure TLS communication for the metrics endpoint.
+Changing tls.enabled option will force a rollout of all instances.</p>
+</td>
+</tr>
 <tr><td><code>podMonitorMetricRelabelings</code><br/>
 <a href="https://pkg.go.dev/github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1#RelabelConfig"><i>[]github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1.RelabelConfig</i></a>
 </td>
diff --git a/docs/src/monitoring.md b/docs/src/monitoring.md
index 0268f552..06eed74f 100644
--- a/docs/src/monitoring.md
+++ b/docs/src/monitoring.md
@@ -9,7 +9,7 @@
 ## Monitoring Instances
 
 For each PostgreSQL instance, the operator provides an exporter of metrics for
-[Prometheus](https://prometheus.io/) via HTTP, on port 9187, named `metrics`.
+[Prometheus](https://prometheus.io/) via HTTP or HTTPS, on port 9187, named `metrics`.
 The operator comes with a [predefined set of metrics](#predefined-set-of-metrics), as well as a highly
 configurable and customizable system to define additional queries via one or
 more `ConfigMap` or `Secret` resources (see the
@@ -59,15 +59,17 @@ by specifying a list of one or more databases in the `target_databases` option.
 
 A specific PostgreSQL cluster can be monitored using the
 [Prometheus Operator's](https://github.com/prometheus-operator/prometheus-operator) resource 
-[PodMonitor](https://github.com/prometheus-operator/prometheus-operator/blob/v0.47.1/Documentation/api.md#podmonitor).
-A PodMonitor correctly pointing to a Cluster can be automatically created by the operator by setting
-`.spec.monitoring.enablePodMonitor` to `true` in the Cluster resource itself (default: false).
+[PodMonitor](https://github.com/prometheus-operator/prometheus-operator/blob/v0.75.1/Documentation/api.md#podmonitor).
+
+A `PodMonitor` that correctly points to the Cluster can be automatically created by the operator by setting
+`.spec.monitoring.enablePodMonitor` to `true` in the Cluster resource itself (default: `false`).
 
 !!! Important
     Any change to the `PodMonitor` created automatically will be overridden by the Operator at the next reconciliation
     cycle, in case you need to customize it, you can do so as described below.
 
-To deploy a `PodMonitor` for a specific Cluster manually, you can just define it as follows, changing it as needed:
+To deploy a `PodMonitor` for a specific Cluster manually, define it as follows and adjust as needed:
+
 ```yaml
 apiVersion: monitoring.coreos.com/v1
 kind: PodMonitor
@@ -82,14 +84,58 @@ spec:
 ```
 
 !!! Important
-    Make sure you modify the example above with a unique name as well as the
-    correct cluster's namespace and labels (we are using `cluster-example`).
+    Ensure you modify the example above with a unique name, as well as the
+    correct cluster's namespace and labels (e.g., `cluster-example`).
 
 !!! Important
-    Label `postgresql`, used in previous versions of this document, is deprecated
-    and will be removed in the future. Please use the label `cnpg.io/cluster`
+    The `postgresql` label, used in previous versions of this document, is deprecated
+    and will be removed in the future. Please use the `cnpg.io/cluster` label
     instead to select the instances.
 
+### Enabling TLS on the Metrics Port
+
+To enable TLS communication on the metrics port, configure the `.spec.monitoring.tls.enabled`
+setting to `true`. This setup ensures that the metrics exporter uses the same
+server certificate used by PostgreSQL to secure communication on port 5432.
+
+!!! Important
+    Changing the `.spec.monitoring.tls.enabled` setting will trigger a rolling restart of the Cluster.
+
+If the `PodMonitor` is managed by the operator (`.spec.monitoring.enablePodMonitor` set to `true`),
+it will automatically contain the necessary configurations to access the metrics via TLS.
+
+To manually deploy a `PodMonitor` suitable for reading metrics via TLS, define it as follows and
+adjust as needed:
+
+```yaml
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+  name: cluster-example
+spec:
+  selector:
+    matchLabels:
+      "cnpg.io/cluster": cluster-example
+  podMetricsEndpoints:
+  - port: metrics
+    scheme: https
+    tlsConfig:
+      ca:
+        secret:
+          name: cluster-example-ca
+          key: ca.crt
+      serverName: cluster-example-rw
+```
+
+!!! Important
+    Ensure you modify the example above with a unique name, as well as the
+    correct Cluster's namespace and labels (e.g., `cluster-example`).
+
+!!! Important
+    The `serverName` field in the metrics endpoint must match one of the names
+    defined in the server certificate. If the default certificate is in use,
+    the `serverName` value should be in the format `<cluster-name>-rw`.
+
 ### Predefined set of metrics
 
 Every PostgreSQL instance exporter automatically exposes a set of predefined
@@ -746,6 +792,11 @@ And then run:
 kubectl exec -ti curl -- curl -s ${POD_IP}:9187/metrics
 ```
 
+If you enabled TLS metrics, run instead:
+```shell
+kubectl exec -ti curl -- curl -sk https://${POD_IP}:9187/metrics
+```
+
 In case you want to access the metrics of the operator, you need to point
 to the pod where the operator is running, and use TCP port 8080 as target.
 
diff --git a/docs/src/security.md b/docs/src/security.md
index cdcc78d1..42472be2 100644
--- a/docs/src/security.md
+++ b/docs/src/security.md
@@ -377,18 +377,18 @@ section of the Kubernetes documentation for further information.
 CloudNativePG exposes ports at operator, instance manager and operand
 levels, as listed in the table below:
 
-System           | Port number  | Exposing            |  Name               |  Certificates                  |  Authentication
-:--------------- | :----------- | :------------------ | :------------------ | :----------------------------  | :--------------
-operator         | 9443         | webhook server      | `webhook-server`    |  TLS                           | Yes
-operator         | 8080         | metrics             | `metrics`           |  no TLS                        | No
-instance manager | 9187         | metrics             | `metrics`           |  no TLS                        | No
-instance manager | 8000         | status              | `status`            |  TLS (no TLS in old releases)  | No
-operand          | 5432         | PostgreSQL instance | `postgresql`        |  optional TLS                  | Yes
+| System           | Port number | Exposing            | Name             | TLS      | Authentication |
+|:-----------------|:------------|:--------------------|:-----------------|:---------|:---------------|
+| operator         | 9443        | webhook server      | `webhook-server` | Yes      | Yes            |
+| operator         | 8080        | metrics             | `metrics`        | No       | No             |
+| instance manager | 9187        | metrics             | `metrics`        | Optional | No             |
+| instance manager | 8000        | status              | `status`         | Yes      | No             |
+| operand          | 5432        | PostgreSQL instance | `postgresql`     | Optional | Yes            |
 
 ### PostgreSQL
 
 The current implementation of CloudNativePG automatically creates
-passwords and `.pgpass` files for the the database owner and, only
+passwords and `.pgpass` files for the database owner and, only
 if requested by setting `enableSuperuserAccess` to `true`, for the
 `postgres` superuser.
 
diff --git a/internal/cmd/manager/instance/join/cmd.go b/internal/cmd/manager/instance/join/cmd.go
index 844a5270..e8faf0d3 100644
--- a/internal/cmd/manager/instance/join/cmd.go
+++ b/internal/cmd/manager/instance/join/cmd.go
@@ -104,14 +104,10 @@ func joinSubCommand(ctx context.Context, instance *postgres.Instance, info postg
 		return err
 	}
 
-	metricServer, err := metricserver.New(instance)
-	if err != nil {
-		return err
-	}
-
 	// Create a fake reconciler just to download the secrets and
 	// the cluster definition
-	reconciler := controller.NewInstanceReconciler(instance, client, metricServer)
+	metricExporter := metricserver.NewExporter(instance)
+	reconciler := controller.NewInstanceReconciler(instance, client, metricExporter)
 
 	// Download the cluster definition from the API server
 	var cluster apiv1.Cluster
diff --git a/internal/cmd/manager/instance/run/cmd.go b/internal/cmd/manager/instance/run/cmd.go
index 676a3c9a..47743827 100644
--- a/internal/cmd/manager/instance/run/cmd.go
+++ b/internal/cmd/manager/instance/run/cmd.go
@@ -76,6 +76,7 @@ func NewCmd() *cobra.Command {
 	var clusterName string
 	var namespace string
 	var statusPortTLS bool
+	var metricsPortTLS bool
 
 	cmd := &cobra.Command{
 		Use: "run [flags]",
@@ -94,6 +95,7 @@ func NewCmd() *cobra.Command {
 			instance.PodName = podName
 			instance.ClusterName = clusterName
 			instance.StatusPortTLS = statusPortTLS
+			instance.MetricsPortTLS = metricsPortTLS
 
 			err := retry.OnError(retry.DefaultRetry, isRunSubCommandRetryable, func() error {
 				return runSubCommand(ctx, instance)
@@ -123,6 +125,8 @@ func NewCmd() *cobra.Command {
 		"the cluster and of the Pod in k8s")
 	cmd.Flags().BoolVar(&statusPortTLS, "status-port-tls", false,
 		"Enable TLS for communicating with the operator")
+	cmd.Flags().BoolVar(&metricsPortTLS, "metrics-port-tls", false,
+		"Enable TLS for metrics scraping")
 	return cmd
 }
 
@@ -174,15 +178,11 @@ func runSubCommand(ctx context.Context, instance *postgres.Instance) error {
 		return err
 	}
 
-	metricsServer, err := metricserver.New(instance)
-	if err != nil {
-		return err
-	}
-
 	postgresStartConditions := concurrency.MultipleExecuted{}
 	exitedConditions := concurrency.MultipleExecuted{}
 
-	reconciler := controller.NewInstanceReconciler(instance, mgr.GetClient(), metricsServer)
+	metricsExporter := metricserver.NewExporter(instance)
+	reconciler := controller.NewInstanceReconciler(instance, mgr.GetClient(), metricsExporter)
 	err = ctrl.NewControllerManagedBy(mgr).
 		For(&apiv1.Cluster{}).
 		Complete(reconciler)
@@ -227,11 +227,6 @@ func runSubCommand(ctx context.Context, instance *postgres.Instance) error {
 		return err
 	}
 
-	if err = mgr.Add(metricsServer); err != nil {
-		setupLog.Error(err, "unable to add metrics webserver runnable")
-		return err
-	}
-
 	if err = mgr.Add(lifecycle.NewPostgresOrphansReaper(instance)); err != nil {
 		setupLog.Error(err, "unable to create zombie reaper")
 		return err
@@ -275,6 +270,15 @@ func runSubCommand(ctx context.Context, instance *postgres.Instance) error {
 		return err
 	}
 
+	metricsServer, err := metricserver.New(instance, metricsExporter)
+	if err != nil {
+		return err
+	}
+	if err = mgr.Add(metricsServer); err != nil {
+		setupLog.Error(err, "unable to add local webserver runnable")
+		return err
+	}
+
 	setupLog.Info("starting tablespace manager")
 	if err := tablespaces.NewTablespaceReconciler(instance, mgr.GetClient()).
 		SetupWithManager(mgr); err != nil {
diff --git a/internal/management/controller/manager.go b/internal/management/controller/manager.go
index 5c803756..9ea68245 100644
--- a/internal/management/controller/manager.go
+++ b/internal/management/controller/manager.go
@@ -52,7 +52,7 @@ type InstanceReconciler struct {
 func NewInstanceReconciler(
 	instance *postgres.Instance,
 	client ctrl.Client,
-	server *metricserver.MetricsServer,
+	metricsExporter *metricserver.Exporter,
 ) *InstanceReconciler {
 	return &InstanceReconciler{
 		instance:              instance,
@@ -60,7 +60,7 @@ func NewInstanceReconciler(
 		secretVersions:        make(map[string]string),
 		extensionStatus:       make(map[string]bool),
 		systemInitialization:  concurrency.NewExecuted(),
-		metricsServerExporter: server.GetExporter(),
+		metricsServerExporter: metricsExporter,
 	}
 }
 
diff --git a/pkg/management/postgres/instance.go b/pkg/management/postgres/instance.go
index b9db62f0..6ba9c87e 100644
--- a/pkg/management/postgres/instance.go
+++ b/pkg/management/postgres/instance.go
@@ -205,6 +205,9 @@ type Instance struct {
 	// StatusPortTLS enables TLS on the status port used to communicate with the operator
 	StatusPortTLS bool
 
+	// MetricsPortTLS enables TLS on the port used to publish metrics over HTTP/HTTPS
+	MetricsPortTLS bool
+
 	// ServerCertificate is the certificate we use to serve https connections
 	ServerCertificate *tls.Certificate
 }
diff --git a/pkg/management/postgres/webserver/local.go b/pkg/management/postgres/webserver/local.go
index a38485bf..5f07e2a1 100644
--- a/pkg/management/postgres/webserver/local.go
+++ b/pkg/management/postgres/webserver/local.go
@@ -69,7 +69,7 @@ func NewLocalWebServer(instance *postgres.Instance) (*Webserver, error) {
 		ReadTimeout:       DefaultReadTimeout,
 	}
 
-	webserver := NewWebServer(instance, server)
+	webserver := NewWebServer(server)
 
 	return webserver, nil
 }
diff --git a/pkg/management/postgres/webserver/metricserver/metrics.go b/pkg/management/postgres/webserver/metricserver/metrics.go
index 67f0f782..7e3ac339 100644
--- a/pkg/management/postgres/webserver/metricserver/metrics.go
+++ b/pkg/management/postgres/webserver/metricserver/metrics.go
@@ -17,6 +17,7 @@ limitations under the License.
 package metricserver
 
 import (
+	"crypto/tls"
 	"fmt"
 	"net/http"
 
@@ -40,9 +41,8 @@ type MetricsServer struct {
 
 // New configure the web statusServer for a certain PostgreSQL instance, and
 // must be invoked before starting the real web statusServer
-func New(serverInstance *postgres.Instance) (*MetricsServer, error) {
+func New(serverInstance *postgres.Instance, exporter *Exporter) (*MetricsServer, error) {
 	registry := prometheus.NewRegistry()
-	exporter := NewExporter(serverInstance)
 	if err := registry.Register(exporter); err != nil {
 		return nil, fmt.Errorf("while registering PostgreSQL exporters: %w", err)
 	}
@@ -59,16 +59,19 @@ func New(serverInstance *postgres.Instance) (*MetricsServer, error) {
 		ReadHeaderTimeout: webserver.DefaultReadHeaderTimeout,
 	}
 
+	if serverInstance.MetricsPortTLS {
+		server.TLSConfig = &tls.Config{
+			MinVersion: tls.VersionTLS13,
+			GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
+				return serverInstance.ServerCertificate, nil
+			},
+		}
+	}
+
 	metricServer := &MetricsServer{
-		Webserver: webserver.NewWebServer(serverInstance, server),
+		Webserver: webserver.NewWebServer(server),
 		exporter:  exporter,
 	}
 
 	return metricServer, nil
 }
-
-// GetExporter get the exporter used for metrics. If the web statusServer still
-// has not started, the exporter is nil
-func (ms *MetricsServer) GetExporter() *Exporter {
-	return ms.exporter
-}
diff --git a/pkg/management/postgres/webserver/remote.go b/pkg/management/postgres/webserver/remote.go
index 9e05af2c..5210dd07 100644
--- a/pkg/management/postgres/webserver/remote.go
+++ b/pkg/management/postgres/webserver/remote.go
@@ -108,7 +108,7 @@ func NewRemoteWebServer(
 		}
 	}
 
-	return NewWebServer(instance, server), nil
+	return NewWebServer(server), nil
 }
 
 func (ws *remoteWebserverEndpoints) isServerHealthy(w http.ResponseWriter, _ *http.Request) {
diff --git a/pkg/management/postgres/webserver/webserver.go b/pkg/management/postgres/webserver/webserver.go
index 41070a3e..212a706b 100644
--- a/pkg/management/postgres/webserver/webserver.go
+++ b/pkg/management/postgres/webserver/webserver.go
@@ -19,12 +19,12 @@ package webserver
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"time"
 
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/log"
-	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres"
 )
 
 const (
@@ -65,26 +65,23 @@ func (body Response[T]) EnsureDataIsPresent() error {
 	return fmt.Errorf("encounteered an empty body while expecting it to not be empty")
 }
 
-// Webserver contains a server that interacts with postgres instance
+// Webserver wraps a webserver to make it a kubernetes Runnable
 type Webserver struct {
-	// instance is the PostgreSQL instance to be collected
-	instance *postgres.Instance
-	server   *http.Server
+	server *http.Server
 }
 
-// NewWebServer creates a Webserver given a postgres.Instance and a http.Server
-func NewWebServer(instance *postgres.Instance, server *http.Server) *Webserver {
+// NewWebServer creates a Webserver as a Kubernetes Runnable, given a http.Server
+func NewWebServer(server *http.Server) *Webserver {
 	return &Webserver{
-		instance: instance,
-		server:   server,
+		server: server,
 	}
 }
 
-// Start implements the runnable interface
+// Start starts a webserver listener, implementing the K8s runnable interface
 func (ws *Webserver) Start(ctx context.Context) error {
 	errChan := make(chan error, 1)
 	go func() {
-		log.Info("Starting webserver", "address", ws.server.Addr)
+		log.Info("Starting webserver", "address", ws.server.Addr, "hasTLS", ws.server.TLSConfig != nil)
 
 		var err error
 		if ws.server.TLSConfig != nil {
@@ -101,7 +98,11 @@ func (ws *Webserver) Start(ctx context.Context) error {
 	// we exit with error code, potentially we could do a retry logic, but rarely a webserver that doesn't start will run
 	// on subsequent tries
 	case err := <-errChan:
-		log.Error(err, "Error while starting the web server", "address", ws.server.Addr)
+		if errors.Is(http.ErrServerClosed, err) {
+			log.Error(err, "Closing the web server", "address", ws.server.Addr)
+		} else {
+			log.Error(err, "Error while running the web server", "address", ws.server.Addr)
+		}
 		return err
 	case <-ctx.Done():
 		if err := ws.server.Shutdown(context.Background()); err != nil {
diff --git a/pkg/specs/podmonitor.go b/pkg/specs/podmonitor.go
index a036e8d1..25770e36 100644
--- a/pkg/specs/podmonitor.go
+++ b/pkg/specs/podmonitor.go
@@ -18,9 +18,12 @@ package specs
 
 import (
 	monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/certs"
 )
 
 // ClusterPodMonitorManager builds the PodMonitor for the cluster resource
@@ -45,6 +48,24 @@ func (c ClusterPodMonitorManager) BuildPodMonitor() *monitoringv1.PodMonitor {
 		Port: "metrics",
 	}
 
+	if c.cluster.IsMetricsTLSEnabled() {
+		endpoint.Scheme = "https"
+		endpoint.TLSConfig = &monitoringv1.SafeTLSConfig{
+			CA: monitoringv1.SecretOrConfigMap{
+				Secret: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: c.cluster.GetServerCASecretName(),
+					},
+					Key: certs.CACertKey,
+				},
+			},
+			ServerName: ptr.To(c.cluster.GetServiceReadWriteName()),
+			// InsecureSkipVerify needs to be set to match the ssl_mode=verify-ca
+			// used by postgres when connecting to the other instances.
+			InsecureSkipVerify: ptr.To(true),
+		}
+	}
+
 	if c.cluster.Spec.Monitoring != nil {
 		endpoint.MetricRelabelConfigs = c.cluster.Spec.Monitoring.PodMonitorMetricRelabelConfigs
 		endpoint.RelabelConfigs = c.cluster.Spec.Monitoring.PodMonitorRelabelConfigs
diff --git a/pkg/specs/pods.go b/pkg/specs/pods.go
index 257104ad..ed6c37c6 100644
--- a/pkg/specs/pods.go
+++ b/pkg/specs/pods.go
@@ -254,6 +254,10 @@ func createPostgresContainers(cluster apiv1.Cluster, envConfig EnvConfig, enable
 		containers[0].Command = append(containers[0].Command, "--status-port-tls")
 	}
 
+	if cluster.IsMetricsTLSEnabled() {
+		containers[0].Command = append(containers[0].Command, "--metrics-port-tls")
+	}
+
 	addManagerLoggingOptions(cluster, &containers[0])
 
 	// if user customizes the liveness probe timeout, we need to adjust the failure threshold



Test patch:
diff --git a/pkg/specs/podmonitor_test.go b/pkg/specs/podmonitor_test.go
index 1da31816..6c486e18 100644
--- a/pkg/specs/podmonitor_test.go
+++ b/pkg/specs/podmonitor_test.go
@@ -18,9 +18,12 @@ package specs
 
 import (
 	monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/certs"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -32,82 +35,130 @@ var _ = Describe("PodMonitor test", func() {
 		clusterName      = "test"
 		clusterNamespace = "test-namespace"
 	)
-	cluster := apiv1.Cluster{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: clusterNamespace,
-			Name:      clusterName,
-		},
-		Spec: apiv1.ClusterSpec{
-			Monitoring: &apiv1.MonitoringConfiguration{
-				EnablePodMonitor: true,
-			},
-		},
-	}
 
-	expectedEndpoint := monitoringv1.PodMetricsEndpoint{Port: "metrics"}
-	metricRelabelings := []monitoringv1.RelabelConfig{
-		{
-			SourceLabels: []monitoringv1.LabelName{"cluster"},
-			TargetLabel:  "cnpg_cluster",
-		},
-		{
-			Regex:  "cluster",
-			Action: "labeldrop",
-		},
+	assertPodMonitorCorrect := func(cluster *apiv1.Cluster, expectedEndpoint monitoringv1.PodMetricsEndpoint) {
+		getMetricRelabelings := func() []monitoringv1.RelabelConfig {
+			return []monitoringv1.RelabelConfig{
+				{
+					SourceLabels: []monitoringv1.LabelName{"cluster"},
+					TargetLabel:  "cnpg_cluster",
+				},
+				{
+					Regex:  "cluster",
+					Action: "labeldrop",
+				},
+			}
+		}
+		getRelabelings := func() []monitoringv1.RelabelConfig {
+			return []monitoringv1.RelabelConfig{
+				{
+					SourceLabels: []monitoringv1.LabelName{"__their_label"},
+					TargetLabel:  "my_label",
+				},
+			}
+		}
+
+		It("should create a valid monitoringv1.PodMonitor object", func() {
+			mgr := NewClusterPodMonitorManager(cluster.DeepCopy())
+			monitor := mgr.BuildPodMonitor()
+			Expect(monitor.Labels[utils.ClusterLabelName]).To(Equal(cluster.Name))
+			Expect(monitor.Spec.Selector.MatchLabels[utils.ClusterLabelName]).To(Equal(cluster.Name))
+			Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(expectedEndpoint))
+		})
+
+		It("should create a monitoringv1.PodMonitor object with MetricRelabelConfigs rules", func() {
+			relabeledCluster := cluster.DeepCopy()
+			relabeledCluster.Spec.Monitoring.PodMonitorMetricRelabelConfigs = getMetricRelabelings()
+			mgr := NewClusterPodMonitorManager(relabeledCluster)
+			monitor := mgr.BuildPodMonitor()
+
+			expectedEndpoint := expectedEndpoint.DeepCopy()
+			expectedEndpoint.MetricRelabelConfigs = getMetricRelabelings()
+			Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
+		})
+
+		It("should create a monitoringv1.PodMonitor object with RelabelConfigs rules", func() {
+			relabeledCluster := cluster.DeepCopy()
+			relabeledCluster.Spec.Monitoring.PodMonitorRelabelConfigs = getRelabelings()
+			mgr := NewClusterPodMonitorManager(relabeledCluster)
+			monitor := mgr.BuildPodMonitor()
+
+			expectedEndpoint := expectedEndpoint.DeepCopy()
+			expectedEndpoint.RelabelConfigs = getRelabelings()
+			Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
+		})
+
+		It("should create a monitoringv1.PodMonitor object with MetricRelabelConfigs and RelabelConfigs rules", func() {
+			relabeledCluster := cluster.DeepCopy()
+			relabeledCluster.Spec.Monitoring.PodMonitorMetricRelabelConfigs = getMetricRelabelings()
+			relabeledCluster.Spec.Monitoring.PodMonitorRelabelConfigs = getRelabelings()
+			mgr := NewClusterPodMonitorManager(relabeledCluster)
+			monitor := mgr.BuildPodMonitor()
+
+			expectedEndpoint := expectedEndpoint.DeepCopy()
+			expectedEndpoint.MetricRelabelConfigs = getMetricRelabelings()
+			expectedEndpoint.RelabelConfigs = getRelabelings()
+			Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
+		})
+
+		It("does not panic if monitoring section is not present", func() {
+			cluster := apiv1.Cluster{}
+			mgr := NewClusterPodMonitorManager(&cluster)
+			Expect(mgr.BuildPodMonitor()).ToNot(BeNil())
+		})
 	}
-	relabelings := []monitoringv1.RelabelConfig{
-		{
-			SourceLabels: []monitoringv1.LabelName{"__their_label"},
-			TargetLabel:  "my_label",
-		},
-	}
-
-	It("should create a valid monitoringv1.PodMonitor object", func() {
-		mgr := NewClusterPodMonitorManager(cluster.DeepCopy())
-		monitor := mgr.BuildPodMonitor()
-		Expect(monitor.Labels[utils.ClusterLabelName]).To(Equal(clusterName))
-		Expect(monitor.Spec.Selector.MatchLabels[utils.ClusterLabelName]).To(Equal(clusterName))
-		Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(expectedEndpoint))
-	})
-
-	It("should create a monitoringv1.PodMonitor object with MetricRelabelConfigs rules", func() {
-		relabeledCluster := cluster.DeepCopy()
-		relabeledCluster.Spec.Monitoring.PodMonitorMetricRelabelConfigs = metricRelabelings
-		mgr := NewClusterPodMonitorManager(relabeledCluster)
-		monitor := mgr.BuildPodMonitor()
-
-		expectedEndpoint := expectedEndpoint.DeepCopy()
-		expectedEndpoint.MetricRelabelConfigs = metricRelabelings
-		Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
-	})
 
-	It("should create a monitoringv1.PodMonitor object with RelabelConfigs rules", func() {
-		relabeledCluster := cluster.DeepCopy()
-		relabeledCluster.Spec.Monitoring.PodMonitorRelabelConfigs = relabelings
-		mgr := NewClusterPodMonitorManager(relabeledCluster)
-		monitor := mgr.BuildPodMonitor()
+	When("not using TLS", func() {
+		cluster := apiv1.Cluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: clusterNamespace,
+				Name:      clusterName,
+			},
+			Spec: apiv1.ClusterSpec{
+				Monitoring: &apiv1.MonitoringConfiguration{
+					EnablePodMonitor: true,
+				},
+			},
+		}
 
-		expectedEndpoint := expectedEndpoint.DeepCopy()
-		expectedEndpoint.RelabelConfigs = relabelings
-		Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
-	})
+		expectedEndpoint := monitoringv1.PodMetricsEndpoint{Port: "metrics"}
 
-	It("should create a monitoringv1.PodMonitor object with MetricRelabelConfigs and RelabelConfigs rules", func() {
-		relabeledCluster := cluster.DeepCopy()
-		relabeledCluster.Spec.Monitoring.PodMonitorMetricRelabelConfigs = metricRelabelings
-		relabeledCluster.Spec.Monitoring.PodMonitorRelabelConfigs = relabelings
-		mgr := NewClusterPodMonitorManager(relabeledCluster)
-		monitor := mgr.BuildPodMonitor()
-
-		expectedEndpoint := expectedEndpoint.DeepCopy()
-		expectedEndpoint.MetricRelabelConfigs = metricRelabelings
-		expectedEndpoint.RelabelConfigs = relabelings
-		Expect(monitor.Spec.PodMetricsEndpoints).To(ContainElement(*expectedEndpoint))
+		assertPodMonitorCorrect(&cluster, expectedEndpoint)
 	})
+	When("TLS is enabled for metrics", func() {
+		cluster := apiv1.Cluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: clusterNamespace,
+				Name:      clusterName,
+			},
+			Spec: apiv1.ClusterSpec{
+				Monitoring: &apiv1.MonitoringConfiguration{
+					EnablePodMonitor: true,
+					TLSConfig: &apiv1.ClusterMonitoringTLSConfiguration{
+						Enabled: true,
+					},
+				},
+			},
+		}
+
+		expectedEndpoint := monitoringv1.PodMetricsEndpoint{
+			Port:   "metrics",
+			Scheme: "https",
+			TLSConfig: &monitoringv1.SafeTLSConfig{
+				CA: monitoringv1.SecretOrConfigMap{
+					Secret: &corev1.SecretKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "test-ca",
+						},
+						Key: certs.CACertKey,
+					},
+				},
+				Cert:               monitoringv1.SecretOrConfigMap{},
+				ServerName:         ptr.To(cluster.GetServiceReadWriteName()),
+				InsecureSkipVerify: ptr.To(true),
+			},
+		}
 
-	It("does not panic if monitoring section is not present", func() {
-		cluster := apiv1.Cluster{}
-		mgr := NewClusterPodMonitorManager(&cluster)
-		Expect(mgr.BuildPodMonitor()).ToNot(BeNil())
+		assertPodMonitorCorrect(&cluster, expectedEndpoint)
 	})
 })