Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: Allow TLS certificate customisation in the PgBouncer `Pooler` resource
### 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?

The current `Pooler` interface in CloudNativePG does not allow users to customise TLS certificates for PgBouncer. This limits the ability to have full control over TLS configuration across the stack (applications → PgBouncer → PostgreSQL).

At the moment, PgBouncer reuses the PostgreSQL server certificates provided by the `Cluster` resource. The only configurable option in the `Pooler` CRD is `.spec.pgbouncer.authQuerySecret`, which is restricted to authentication queries against PostgreSQL (the name is also misleading).

Reasons why this is important:
 
- Organisations often need to manage TLS certificates independently for different stack components.
- Security/compliance requirements may mandate separation between application-side and server-side certificates.
- Consistent support for TLS configuration across PostgreSQL, PgBouncer, and applications will make CNPG more flexible and enterprise-ready.


### Describe the solution you'd like

Extend the `Pooler` API (`.spec.pgbouncer`) to allow custom TLS configuration by referencing Kubernetes secrets. The following fields could be introduced:

- **`clientTLSSecret`**: Provides PgBouncer’s `client_tls_key_file` (private key) and `client_tls_cert_file` (certificate) used to accept client connections.
- **`clientCASecret`**: Provides PgBouncer’s `client_tls_ca_file`, the root CA for validating client certificates.
- **`serverTLSSecret`**: Provides PgBouncer’s `server_tls_key_file` and `server_tls_cert_file`, used when authenticating against PostgreSQL.
- **`serverCASecret`**: Provides PgBouncer’s `server_tls_ca_file`, the root CA for validating PostgreSQL certificates.

### Describe alternatives you've considered

N/A

### Additional context

Migration considerations:

- `serverTLSSecret` would replace (with broader scope) the existing `authQuerySecret`. 
- `authQuerySecret` should be marked as deprecated, with admission webhooks to guide users during the transition.

### Backport?

No

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

Yes

### Code of Conduct

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

Hints from issue comments:

Solution patch:
diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt
index c0d13600..3b139509 100644
--- a/.wordlist-en-custom.txt
+++ b/.wordlist-en-custom.txt
@@ -81,6 +81,7 @@ ClassName
 ClientCASecret
 ClientCertsCASecret
 ClientReplicationSecret
+ClientTLSSecret
 CloudNativePG
 CloudNativePG's
 ClusterBinding
@@ -690,6 +691,8 @@ cli
 clientCA
 clientCASecret
 clientCaSecretVersion
+clientTLS
+clientTLSSecret
 cloudNativePGCommitHash
 cloudNativePGOperatorHash
 cloudnative
@@ -1145,6 +1148,7 @@ pgbarman
 pgbasebackup
 pgbench
 pgbouncer
+pgbouncer's
 pgdata
 pgpass
 pgstatstatements
diff --git a/api/v1/pooler_funcs.go b/api/v1/pooler_funcs.go
index f1ca476e..1e5795bf 100644
--- a/api/v1/pooler_funcs.go
+++ b/api/v1/pooler_funcs.go
@@ -36,6 +36,46 @@ func (in *Pooler) GetAuthQuerySecretName() string {
 	return in.Spec.Cluster.Name + DefaultPgBouncerPoolerSecretSuffix
 }
 
+// GetServerTLSSecretName returns the specified server TLS secret name
+// for PgBouncer if provided or the default name otherwise.
+func (in *Pooler) GetServerTLSSecretName() string {
+	if in.Spec.PgBouncer != nil && in.Spec.PgBouncer.ServerTLSSecret != nil {
+		return in.Spec.PgBouncer.ServerTLSSecret.Name
+	}
+
+	return ""
+}
+
+// GetServerCASecretNameOrDefault returns the specified server CA secret name
+// for PgBouncer if provided or the default name otherwise.
+func (in *Pooler) GetServerCASecretNameOrDefault(cluster *Cluster) string {
+	if in.Spec.PgBouncer != nil && in.Spec.PgBouncer.ServerCASecret != nil {
+		return in.Spec.PgBouncer.ServerCASecret.Name
+	}
+
+	return cluster.GetServerCASecretName()
+}
+
+// GetClientCASecretNameOrDefault returns the specified client CA secret name
+// for PgBouncer if provided or the default name otherwise.
+func (in *Pooler) GetClientCASecretNameOrDefault(cluster *Cluster) string {
+	if in.Spec.PgBouncer != nil && in.Spec.PgBouncer.ClientCASecret != nil {
+		return in.Spec.PgBouncer.ClientCASecret.Name
+	}
+
+	return cluster.GetClientCASecretName()
+}
+
+// GetClientTLSSecretNameOrDefault returns the specified client TLS secret name
+// for PgBouncer if provided or the default name otherwise.
+func (in *Pooler) GetClientTLSSecretNameOrDefault(cluster *Cluster) string {
+	if in.Spec.PgBouncer != nil && in.Spec.PgBouncer.ClientTLSSecret != nil {
+		return in.Spec.PgBouncer.ClientTLSSecret.Name
+	}
+
+	return cluster.GetServerTLSSecretName()
+}
+
 // GetAuthQuery returns the specified AuthQuery name for PgBouncer
 // if provided or the default name otherwise.
 func (in *Pooler) GetAuthQuery() string {
@@ -54,8 +94,9 @@ func (in *Pooler) IsAutomatedIntegration() bool {
 	}
 	// If the user specified an AuthQuerySecret or an AuthQuery, the integration
 	// is not going to be handled by the operator.
-	if (in.Spec.PgBouncer.AuthQuerySecret != nil && in.Spec.PgBouncer.AuthQuerySecret.Name != "") ||
-		in.Spec.PgBouncer.AuthQuery != "" {
+	if in.Spec.PgBouncer.AuthQuery != "" ||
+		(in.Spec.PgBouncer.AuthQuerySecret != nil && in.Spec.PgBouncer.AuthQuerySecret.Name != "") ||
+		(in.Spec.PgBouncer.ServerTLSSecret != nil && in.Spec.PgBouncer.ServerTLSSecret.Name != "") {
 		return false
 	}
 	return true
diff --git a/api/v1/pooler_types.go b/api/v1/pooler_types.go
index 8ffeb0dd..4d15a26b 100644
--- a/api/v1/pooler_types.go
+++ b/api/v1/pooler_types.go
@@ -163,10 +163,34 @@ type PgBouncerSpec struct {
 	// +optional
 	PoolMode PgBouncerPoolMode `json:"poolMode,omitempty"`
 
+	// ServerTLSSecret, when pointing to a TLS secret, provides pgbouncer's
+	// `server_tls_key_file` and `server_tls_cert_file`, used when
+	// authenticating against PostgreSQL.
+	// +optional
+	ServerTLSSecret *LocalObjectReference `json:"serverTLSSecret,omitempty"`
+
+	// ServerCASecret provides PgBouncer’s server_tls_ca_file, the root
+	// CA for validating PostgreSQL certificates
+	// +optional
+	ServerCASecret *LocalObjectReference `json:"serverCASecret,omitempty"`
+
+	// ClientCASecret provides PgBouncer’s client_tls_ca_file, the root
+	// CA for validating client certificates
+	// +optional
+	ClientCASecret *LocalObjectReference `json:"clientCASecret,omitempty"`
+
+	// ClientTLSSecret provides PgBouncer’s client_tls_key_file (private key)
+	// and client_tls_cert_file (certificate) used to accept client connections
+	// +optional
+	ClientTLSSecret *LocalObjectReference `json:"clientTLSSecret,omitempty"`
+
 	// The credentials of the user that need to be used for the authentication
 	// query. In case it is specified, also an AuthQuery
 	// (e.g. "SELECT usename, passwd FROM pg_catalog.pg_shadow WHERE usename=$1")
 	// has to be specified and no automatic CNPG Cluster integration will be triggered.
+	//
+	// Deprecated.
+	//
 	// +optional
 	AuthQuerySecret *LocalObjectReference `json:"authQuerySecret,omitempty"`
 
@@ -208,6 +232,10 @@ type PoolerStatus struct {
 
 // PoolerSecrets contains the versions of all the secrets used
 type PoolerSecrets struct {
+	// The client TLS secret version
+	// +optional
+	ClientTLS SecretVersion `json:"clientTLS,omitempty"`
+
 	// The server TLS secret version
 	// +optional
 	ServerTLS SecretVersion `json:"serverTLS,omitempty"`
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index 2c1bf602..c64a23a0 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -2121,6 +2121,26 @@ func (in *PgBouncerSecrets) DeepCopy() *PgBouncerSecrets {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PgBouncerSpec) DeepCopyInto(out *PgBouncerSpec) {
 	*out = *in
+	if in.ServerTLSSecret != nil {
+		in, out := &in.ServerTLSSecret, &out.ServerTLSSecret
+		*out = new(LocalObjectReference)
+		**out = **in
+	}
+	if in.ServerCASecret != nil {
+		in, out := &in.ServerCASecret, &out.ServerCASecret
+		*out = new(LocalObjectReference)
+		**out = **in
+	}
+	if in.ClientCASecret != nil {
+		in, out := &in.ClientCASecret, &out.ClientCASecret
+		*out = new(LocalObjectReference)
+		**out = **in
+	}
+	if in.ClientTLSSecret != nil {
+		in, out := &in.ClientTLSSecret, &out.ClientTLSSecret
+		*out = new(LocalObjectReference)
+		**out = **in
+	}
 	if in.AuthQuerySecret != nil {
 		in, out := &in.AuthQuerySecret, &out.AuthQuerySecret
 		*out = new(LocalObjectReference)
@@ -2372,6 +2392,7 @@ func (in *PoolerMonitoringConfiguration) DeepCopy() *PoolerMonitoringConfigurati
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PoolerSecrets) DeepCopyInto(out *PoolerSecrets) {
 	*out = *in
+	out.ClientTLS = in.ClientTLS
 	out.ServerTLS = in.ServerTLS
 	out.ServerCA = in.ServerCA
 	out.ClientCA = in.ClientCA
diff --git a/config/crd/bases/postgresql.cnpg.io_poolers.yaml b/config/crd/bases/postgresql.cnpg.io_poolers.yaml
index bf5ff1b5..ff7943f7 100644
--- a/config/crd/bases/postgresql.cnpg.io_poolers.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_poolers.yaml
@@ -319,6 +319,30 @@ spec:
                       query. In case it is specified, also an AuthQuery
                       (e.g. "SELECT usename, passwd FROM pg_catalog.pg_shadow WHERE usename=$1")
                       has to be specified and no automatic CNPG Cluster integration will be triggered.
+
+                      Deprecated.
+                    properties:
+                      name:
+                        description: Name of the referent.
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  clientCASecret:
+                    description: |-
+                      ClientCASecret provides PgBouncer’s client_tls_ca_file, the root
+                      CA for validating client certificates
+                    properties:
+                      name:
+                        description: Name of the referent.
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  clientTLSSecret:
+                    description: |-
+                      ClientTLSSecret provides PgBouncer’s client_tls_key_file (private key)
+                      and client_tls_cert_file (certificate) used to accept client connections
                     properties:
                       name:
                         description: Name of the referent.
@@ -355,6 +379,29 @@ spec:
                     - session
                     - transaction
                     type: string
+                  serverCASecret:
+                    description: |-
+                      ServerCASecret provides PgBouncer’s server_tls_ca_file, the root
+                      CA for validating PostgreSQL certificates
+                    properties:
+                      name:
+                        description: Name of the referent.
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  serverTLSSecret:
+                    description: |-
+                      ServerTLSSecret, when pointing to a TLS secret, provides pgbouncer's
+                      `server_tls_key_file` and `server_tls_cert_file`, used when
+                      authenticating against PostgreSQL.
+                    properties:
+                      name:
+                        description: Name of the referent.
+                        type: string
+                    required:
+                    - name
+                    type: object
                 type: object
               serviceTemplate:
                 description: Template for the Service to be created
@@ -9203,6 +9250,16 @@ spec:
                         description: The ResourceVersion of the secret
                         type: string
                     type: object
+                  clientTLS:
+                    description: The client TLS secret version
+                    properties:
+                      name:
+                        description: The name of the secret
+                        type: string
+                      version:
+                        description: The ResourceVersion of the secret
+                        type: string
+                    type: object
                   pgBouncerSecrets:
                     description: The version of the secrets used by PgBouncer
                     properties:
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index 1e3e98f0..98e3b3d8 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -4253,6 +4253,39 @@ by pgbouncer</p>
    <p>The pool mode. Default: <code>session</code>.</p>
 </td>
 </tr>
+<tr><td><code>serverTLSSecret</code><br/>
+<a href="https://pkg.go.dev/github.com/cloudnative-pg/machinery/pkg/api/#LocalObjectReference"><i>github.com/cloudnative-pg/machinery/pkg/api.LocalObjectReference</i></a>
+</td>
+<td>
+   <p>ServerTLSSecret, when pointing to a TLS secret, provides pgbouncer's
+<code>server_tls_key_file</code> and <code>server_tls_cert_file</code>, used when
+authenticating against PostgreSQL.</p>
+</td>
+</tr>
+<tr><td><code>serverCASecret</code><br/>
+<a href="https://pkg.go.dev/github.com/cloudnative-pg/machinery/pkg/api/#LocalObjectReference"><i>github.com/cloudnative-pg/machinery/pkg/api.LocalObjectReference</i></a>
+</td>
+<td>
+   <p>ServerCASecret provides PgBouncer’s server_tls_ca_file, the root
+CA for validating PostgreSQL certificates</p>
+</td>
+</tr>
+<tr><td><code>clientCASecret</code><br/>
+<a href="https://pkg.go.dev/github.com/cloudnative-pg/machinery/pkg/api/#LocalObjectReference"><i>github.com/cloudnative-pg/machinery/pkg/api.LocalObjectReference</i></a>
+</td>
+<td>
+   <p>ClientCASecret provides PgBouncer’s client_tls_ca_file, the root
+CA for validating client certificates</p>
+</td>
+</tr>
+<tr><td><code>clientTLSSecret</code><br/>
+<a href="https://pkg.go.dev/github.com/cloudnative-pg/machinery/pkg/api/#LocalObjectReference"><i>github.com/cloudnative-pg/machinery/pkg/api.LocalObjectReference</i></a>
+</td>
+<td>
+   <p>ClientTLSSecret provides PgBouncer’s client_tls_key_file (private key)
+and client_tls_cert_file (certificate) used to accept client connections</p>
+</td>
+</tr>
 <tr><td><code>authQuerySecret</code><br/>
 <a href="https://pkg.go.dev/github.com/cloudnative-pg/machinery/pkg/api/#LocalObjectReference"><i>github.com/cloudnative-pg/machinery/pkg/api.LocalObjectReference</i></a>
 </td>
@@ -4261,6 +4294,7 @@ by pgbouncer</p>
 query. In case it is specified, also an AuthQuery
 (e.g. &quot;SELECT usename, passwd FROM pg_catalog.pg_shadow WHERE usename=$1&quot;)
 has to be specified and no automatic CNPG Cluster integration will be triggered.</p>
+<p>Deprecated.</p>
 </td>
 </tr>
 <tr><td><code>authQuery</code><br/>
@@ -4564,6 +4598,13 @@ part for now.</p>
 <table class="table">
 <thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
 <tbody>
+<tr><td><code>clientTLS</code><br/>
+<a href="#postgresql-cnpg-io-v1-SecretVersion"><i>SecretVersion</i></a>
+</td>
+<td>
+   <p>The client TLS secret version</p>
+</td>
+</tr>
 <tr><td><code>serverTLS</code><br/>
 <a href="#postgresql-cnpg-io-v1-SecretVersion"><i>SecretVersion</i></a>
 </td>
diff --git a/docs/src/samples/pooler-external.yaml b/docs/src/samples/pooler-external.yaml
index 5a317158..aadfa063 100644
--- a/docs/src/samples/pooler-external.yaml
+++ b/docs/src/samples/pooler-external.yaml
@@ -18,4 +18,3 @@ spec:
     parameters:
       max_client_conn: "1000"
       default_pool_size: "10"
-  
\ No newline at end of file
diff --git a/internal/controller/pooler_controller.go b/internal/controller/pooler_controller.go
index 99899587..bff0745e 100644
--- a/internal/controller/pooler_controller.go
+++ b/internal/controller/pooler_controller.go
@@ -100,14 +100,9 @@ func (r *PoolerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, fmt.Errorf("while getting managed resources: %w", err)
 	}
 
-	if resources.Cluster == nil {
-		contextLogger.Info("Cluster not found, will retry in 30 seconds", "cluster", pooler.Spec.Cluster.Name)
-		return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
-	}
-
-	if resources.AuthUserSecret == nil {
-		contextLogger.Info("AuthUserSecret not found, waiting 30 seconds", "secret", pooler.GetAuthQuerySecretName())
-		return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
+	// Early exit if some required prerequisite resources are not yet available
+	if res := r.waitForPrerequisites(ctx, &pooler, resources); res != nil {
+		return *res, nil
 	}
 
 	if res := r.ensureManagedResourcesAreOwned(ctx, pooler, resources); !res.IsZero() {
@@ -213,6 +208,63 @@ func (r *PoolerReconciler) ensureManagedResourcesAreOwned(
 	return ctrl.Result{RequeueAfter: 120 * time.Second}
 }
 
+// waitForPrerequisites centralizes the early-return checks for missing dependent resources
+// to keep Reconcile lean. It logs a concise message and instructs the controller to
+// requeue after a short delay when something is missing.
+// Returns a non-nil *ctrl.Result when it requested a requeue; otherwise nil.
+func (r *PoolerReconciler) waitForPrerequisites(
+	ctx context.Context,
+	pooler *apiv1.Pooler,
+	resources *poolerManagedResources,
+) *ctrl.Result {
+	contextLogger := log.FromContext(ctx)
+	waitResult := &ctrl.Result{RequeueAfter: 30 * time.Second}
+
+	if resources.Cluster == nil {
+		contextLogger.Info("Cluster not found, will retry in 30 seconds",
+			"cluster", pooler.Spec.Cluster.Name)
+		return waitResult
+	}
+
+	// For automated integration, we need AuthUserSecret
+	if pooler.IsAutomatedIntegration() && resources.AuthUserSecret == nil {
+		contextLogger.Info("AuthUserSecret not found, waiting 30 seconds",
+			"secret", pooler.GetAuthQuerySecretName())
+		return waitResult
+	}
+
+	// For manual TLS authentication to PostgreSQL, we need ServerTLSSecret
+	if pooler.GetServerTLSSecretName() != "" && resources.ServerTLSSecret == nil {
+		contextLogger.Info("ServerTLSSecret not found, waiting 30 seconds",
+			"secret", pooler.GetServerTLSSecretName())
+		return waitResult
+	}
+
+	// Always required: TLS certificates for accepting client connections
+	if resources.ClientTLSSecret == nil {
+		contextLogger.Info(
+			"ClientTLSSecret not found, waiting 30 seconds",
+			"secret", pooler.GetClientTLSSecretNameOrDefault(resources.Cluster))
+		return waitResult
+	}
+
+	if resources.ClientCASecret == nil {
+		contextLogger.Info(
+			"ClientCASecret not found, waiting 30 seconds",
+			"secret", pooler.GetClientCASecretNameOrDefault(resources.Cluster))
+		return waitResult
+	}
+
+	if resources.ServerCASecret == nil {
+		contextLogger.Info(
+			"ServerCASecret not found, waiting 30 seconds",
+			"secret", pooler.GetServerCASecretNameOrDefault(resources.Cluster))
+		return waitResult
+	}
+
+	return nil
+}
+
 // mapSecretToPooler returns a function mapping secrets events to the poolers using them
 func (r *PoolerReconciler) mapSecretToPooler() handler.MapFunc {
 	return func(ctx context.Context, obj client.Object) (result []reconcile.Request) {
@@ -265,6 +317,16 @@ func getPoolersUsingSecret(poolers apiv1.PoolerList, secret *corev1.Secret) (req
 			)
 			continue
 		}
+
+		if pooler.GetServerTLSSecretName() == secret.Name {
+			requests = append(requests,
+				types.NamespacedName{
+					Name:      pooler.Name,
+					Namespace: pooler.Namespace,
+				},
+			)
+			continue
+		}
 	}
 	return requests
 }
diff --git a/internal/controller/pooler_resources.go b/internal/controller/pooler_resources.go
index eef61914..b1598feb 100644
--- a/internal/controller/pooler_resources.go
+++ b/internal/controller/pooler_resources.go
@@ -38,6 +38,20 @@ type poolerManagedResources struct {
 	// the auth_query connection
 	AuthUserSecret *corev1.Secret
 
+	// This is the secret that is being used to authenticate
+	// connections from pgbouncer to the PostgreSQL Server.
+	ServerTLSSecret *corev1.Secret
+
+	// This is the root certificate to validate PostgreSQL
+	// server certificates.
+	ServerCASecret *corev1.Secret
+
+	// This is the certificate for PgBouncer to accept client connections.
+	ClientTLSSecret *corev1.Secret
+
+	// This is the root certificate to validate client certificates.
+	ClientCASecret *corev1.Secret
+
 	// This is the pgbouncer deployment
 	Deployment *appsv1.Deployment
 
@@ -56,11 +70,19 @@ type poolerManagedResources struct {
 
 // getManagedResources detects the list of the resources created and manager
 // by this pooler
-func (r *PoolerReconciler) getManagedResources(ctx context.Context,
+func (r *PoolerReconciler) getManagedResources(
+	ctx context.Context,
 	pooler *apiv1.Pooler,
 ) (result *poolerManagedResources, err error) {
 	result = &poolerManagedResources{}
 
+	// Get the referenced cluster
+	result.Cluster, err = getClusterOrNil(
+		ctx, r.Client, client.ObjectKey{Name: pooler.Spec.Cluster.Name, Namespace: pooler.Namespace})
+	if err != nil {
+		return nil, err
+	}
+
 	// Get the auth query secret if any
 	result.AuthUserSecret, err = getSecretOrNil(
 		ctx, r.Client, client.ObjectKey{Name: pooler.GetAuthQuerySecretName(), Namespace: pooler.Namespace})
@@ -68,6 +90,55 @@ func (r *PoolerReconciler) getManagedResources(ctx context.Context,
 		return nil, err
 	}
 
+	// Get the server TLS secret for manual PostgreSQL authentication if specified
+	serverTLSSecretName := pooler.GetServerTLSSecretName()
+	if serverTLSSecretName != "" {
+		result.ServerTLSSecret, err = getSecretOrNil(
+			ctx, r.Client, client.ObjectKey{
+				Name:      serverTLSSecretName,
+				Namespace: pooler.Namespace,
+			},
+		)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Get the server CA secret
+	result.ServerCASecret, err = getSecretOrNil(
+		ctx,
+		r.Client,
+		client.ObjectKey{
+			Name:      pooler.GetServerCASecretNameOrDefault(result.Cluster),
+			Namespace: pooler.Namespace,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get the client CA secret
+	result.ClientCASecret, err = getSecretOrNil(
+		ctx, r.Client, client.ObjectKey{
+			Name:      pooler.GetClientCASecretNameOrDefault(result.Cluster),
+			Namespace: pooler.Namespace,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get the client TLS secret
+	result.ClientTLSSecret, err = getSecretOrNil(
+		ctx, r.Client, client.ObjectKey{
+			Name:      pooler.GetClientTLSSecretNameOrDefault(result.Cluster),
+			Namespace: pooler.Namespace,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
 	// Get the pooler deployment
 	result.Deployment, err = getDeploymentOrNil(
 		ctx, r.Client, client.ObjectKey{Name: pooler.Name, Namespace: pooler.Namespace})
@@ -82,13 +153,6 @@ func (r *PoolerReconciler) getManagedResources(ctx context.Context,
 		return nil, err
 	}
 
-	// Get the referenced cluster
-	result.Cluster, err = getClusterOrNil(
-		ctx, r.Client, client.ObjectKey{Name: pooler.Spec.Cluster.Name, Namespace: pooler.Namespace})
-	if err != nil {
-		return nil, err
-	}
-
 	result.ServiceAccount, err = getServiceAccountOrNil(
 		ctx, r.Client, client.ObjectKey{Name: pooler.Name, Namespace: pooler.Namespace})
 	if err != nil {
diff --git a/internal/controller/pooler_status.go b/internal/controller/pooler_status.go
index a1b06ebc..dd362475 100644
--- a/internal/controller/pooler_status.go
+++ b/internal/controller/pooler_status.go
@@ -49,18 +49,31 @@ func (r *PoolerReconciler) updatePoolerStatus(
 		}
 	}
 
-	if cluster := resources.Cluster; cluster != nil {
-		updatedStatus.Secrets.ServerTLS = apiv1.SecretVersion{
-			Name:    cluster.GetServerTLSSecretName(),
-			Version: cluster.Status.SecretsResourceVersion.ServerSecretVersion,
-		}
+	if resources.ServerCASecret != nil {
 		updatedStatus.Secrets.ServerCA = apiv1.SecretVersion{
-			Name:    cluster.GetServerCASecretName(),
-			Version: cluster.Status.SecretsResourceVersion.ServerCASecretVersion,
+			Name:    resources.ServerCASecret.Name,
+			Version: resources.ServerCASecret.ResourceVersion,
 		}
+	}
+
+	if resources.ClientCASecret != nil {
 		updatedStatus.Secrets.ClientCA = apiv1.SecretVersion{
-			Name:    cluster.GetClientCASecretName(),
-			Version: cluster.Status.SecretsResourceVersion.ClientCASecretVersion,
+			Name:    resources.ClientCASecret.Name,
+			Version: resources.ClientCASecret.ResourceVersion,
+		}
+	}
+
+	if resources.ClientTLSSecret != nil {
+		updatedStatus.Secrets.ClientTLS = apiv1.SecretVersion{
+			Name:    resources.ClientTLSSecret.Name,
+			Version: resources.ClientTLSSecret.ResourceVersion,
+		}
+	}
+
+	if resources.ServerTLSSecret != nil {
+		updatedStatus.Secrets.ServerTLS = apiv1.SecretVersion{
+			Name:    resources.ServerTLSSecret.Name,
+			Version: resources.ServerTLSSecret.ResourceVersion,
 		}
 	}
 
diff --git a/internal/pgbouncer/management/controller/secrets.go b/internal/pgbouncer/management/controller/secrets.go
index a12dd6e3..9d8f41cf 100644
--- a/internal/pgbouncer/management/controller/secrets.go
+++ b/internal/pgbouncer/management/controller/secrets.go
@@ -38,21 +38,36 @@ func getSecrets(ctx context.Context, client ctrl.Client, pooler *apiv1.Pooler) (
 		return nil, fmt.Errorf("status not populated yet")
 	}
 
+	result := &config.Secrets{}
+
 	var (
-		authQuerySecret  corev1.Secret
-		serverCASecret   corev1.Secret
-		serverCertSecret corev1.Secret
-		clientCASecret   corev1.Secret
+		authQuerySecret corev1.Secret
+		serverCASecret  corev1.Secret
+		clientTLSSecret corev1.Secret
+		clientCASecret  corev1.Secret
 	)
 
-	authQuerySecretName := pooler.GetAuthQuerySecretName()
-	if err := client.Get(ctx,
-		types.NamespacedName{
-			Name:      authQuerySecretName,
-			Namespace: pooler.Namespace,
-		},
-		&authQuerySecret); err != nil {
-		return nil, fmt.Errorf("while getting auth query secret %s: %w", authQuerySecretName, err)
+	if pooler.Status.Secrets.ServerTLS.Name == "" {
+		authQuerySecretName := pooler.GetAuthQuerySecretName()
+		if err := client.Get(ctx,
+			types.NamespacedName{
+				Name:      authQuerySecretName,
+				Namespace: pooler.Namespace,
+			},
+			&authQuerySecret); err != nil {
+			return nil, fmt.Errorf("while getting auth query secret: %w", err)
+		}
+		result.AuthQuery = &authQuerySecret
+	}
+
+	if pooler.Status.Secrets.ServerTLS.Name != "" {
+		var serverTLSSecret corev1.Secret
+		if err := client.Get(ctx,
+			types.NamespacedName{Name: pooler.Status.Secrets.ServerTLS.Name, Namespace: pooler.Namespace},
+			&serverTLSSecret); err != nil {
+			return nil, fmt.Errorf("while getting server TLS secret: %w", err)
+		}
+		result.ServerTLS = &serverTLSSecret
 	}
 
 	if err := client.Get(ctx,
@@ -60,23 +75,21 @@ func getSecrets(ctx context.Context, client ctrl.Client, pooler *apiv1.Pooler) (
 		&serverCASecret); err != nil {
 		return nil, fmt.Errorf("while getting server CA secret: %w", err)
 	}
+	result.ServerCA = &serverCASecret
 
 	if err := client.Get(ctx,
-		types.NamespacedName{Name: pooler.Status.Secrets.ServerTLS.Name, Namespace: pooler.Namespace},
-		&serverCertSecret); err != nil {
-		return nil, fmt.Errorf("while getting server cert secret: %w", err)
+		types.NamespacedName{Name: pooler.Status.Secrets.ClientTLS.Name, Namespace: pooler.Namespace},
+		&clientTLSSecret); err != nil {
+		return nil, fmt.Errorf("while getting client TLS secret: %w", err)
 	}
+	result.ClientTLS = &clientTLSSecret
 
 	if err := client.Get(ctx,
 		types.NamespacedName{Name: pooler.Status.Secrets.ClientCA.Name, Namespace: pooler.Namespace},
 		&clientCASecret); err != nil {
 		return nil, fmt.Errorf("while getting client CA secret: %w", err)
 	}
+	result.ClientCA = &clientCASecret
 
-	return &config.Secrets{
-		AuthQuery: &authQuerySecret,
-		ServerCA:  &serverCASecret,
-		Client:    &serverCertSecret,
-		ClientCA:  &clientCASecret,
-	}, nil
+	return result, nil
 }
diff --git a/internal/webhook/v1/pooler_webhook.go b/internal/webhook/v1/pooler_webhook.go
index 60a65d20..39f307c9 100644
--- a/internal/webhook/v1/pooler_webhook.go
+++ b/internal/webhook/v1/pooler_webhook.go
@@ -123,18 +123,11 @@ func (v *PoolerCustomValidator) ValidateCreate(_ context.Context, obj runtime.Ob
 	}
 	poolerLog.Info("Validation for Pooler upon creation", "name", pooler.GetName(), "namespace", pooler.GetNamespace())
 
-	var warns admission.Warnings
-	if !pooler.IsAutomatedIntegration() {
-		poolerLog.Info("Pooler not automatically configured, manual configuration required",
-			"name", pooler.Name, "namespace", pooler.Namespace, "cluster", pooler.Spec.Cluster.Name)
-		warns = append(warns, fmt.Sprintf("The operator won't handle the Pooler %q integration with the Cluster %q (%q). "+
-			"Manually configure it as described in the docs.", pooler.Name, pooler.Spec.Cluster.Name, pooler.Namespace))
-	}
+	warns := v.getAdmissionWarnings(pooler)
 
 	warns = append(warns, v.validateDeprecatedMonitoringFields(pooler)...)
 
 	allErrs := v.validate(pooler)
-
 	if len(allErrs) == 0 {
 		return warns, nil
 	}
@@ -144,6 +137,27 @@ func (v *PoolerCustomValidator) ValidateCreate(_ context.Context, obj runtime.Ob
 		pooler.Name, allErrs)
 }
 
+// getAdmissionWarnings gets the admission webhook warnings
+func (v *PoolerCustomValidator) getAdmissionWarnings(r *apiv1.Pooler) admission.Warnings {
+	var warns admission.Warnings
+
+	if !r.IsAutomatedIntegration() {
+		poolerLog.Info("Pooler not automatically configured, manual configuration required",
+			"name", r.Name, "namespace", r.Namespace, "cluster", r.Spec.Cluster.Name)
+		warns = append(warns, fmt.Sprintf("The operator won't handle the Pooler %q integration with the Cluster %q (%q). "+
+			"Manually configure it as described in the docs.", r.Name, r.Spec.Cluster.Name, r.Namespace))
+	}
+
+	if r.Spec.PgBouncer != nil &&
+		r.Spec.PgBouncer.AuthQuerySecret != nil && r.Spec.PgBouncer.AuthQuerySecret.Name != "" { //nolint:staticcheck
+		warns = append(
+			warns,
+			"The .spec.pgbouncer.authQuerySecret field has been deprecated")
+	}
+
+	return warns
+}
+
 // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Pooler.
 func (v *PoolerCustomValidator) ValidateUpdate(
 	_ context.Context,
@@ -154,20 +168,14 @@ func (v *PoolerCustomValidator) ValidateUpdate(
 		return nil, fmt.Errorf("expected a Pooler object for the newObj but got %T", newObj)
 	}
 
-	oldPooler, ok := oldObj.(*apiv1.Pooler)
+	_, ok = oldObj.(*apiv1.Pooler)
 	if !ok {
 		return nil, fmt.Errorf("expected a Pooler object for the oldObj but got %T", oldObj)
 	}
 
 	poolerLog.Info("Validation for Pooler upon update", "name", pooler.GetName(), "namespace", pooler.GetNamespace())
 
-	var warns admission.Warnings
-	if oldPooler.IsAutomatedIntegration() && !pooler.IsAutomatedIntegration() {
-		poolerLog.Info("Pooler not automatically configured, manual configuration required",
-			"name", pooler.Name, "namespace", pooler.Namespace, "cluster", pooler.Spec.Cluster.Name)
-		warns = append(warns, fmt.Sprintf("The operator won't handle the Pooler %q integration with the Cluster %q (%q). "+
-			"Manually configure it as described in the docs.", pooler.Name, pooler.Spec.Cluster.Name, pooler.Namespace))
-	}
+	warns := v.getAdmissionWarnings(pooler)
 
 	warns = append(warns, v.validateDeprecatedMonitoringFields(pooler)...)
 
@@ -195,21 +203,27 @@ func (v *PoolerCustomValidator) ValidateDelete(_ context.Context, obj runtime.Ob
 }
 
 func (v *PoolerCustomValidator) validatePgBouncer(r *apiv1.Pooler) field.ErrorList {
-	var result field.ErrorList
-	switch {
-	case r.Spec.PgBouncer == nil:
-		result = append(result,
+	if r.Spec.PgBouncer == nil {
+		return field.ErrorList{
 			field.Invalid(
 				field.NewPath("spec", "pgbouncer"),
-				"", "required pgbouncer configuration"))
-	case r.Spec.PgBouncer.AuthQuerySecret != nil && r.Spec.PgBouncer.AuthQuerySecret.Name != "" &&
-		r.Spec.PgBouncer.AuthQuery == "":
+				"",
+				"required pgbouncer configuration",
+			),
+		}
+	}
+
+	hasPGAuthentication := (r.Spec.PgBouncer.AuthQuerySecret != nil && r.Spec.PgBouncer.AuthQuerySecret.Name != "") ||
+		(r.Spec.PgBouncer.ServerTLSSecret != nil && r.Spec.PgBouncer.ServerTLSSecret.Name != "")
+
+	var result field.ErrorList
+	switch {
+	case hasPGAuthentication && r.Spec.PgBouncer.AuthQuery == "":
 		result = append(result,
 			field.Invalid(
 				field.NewPath("spec", "pgbouncer", "authQuery"),
 				"", "must specify an auth query when providing an auth query secret"))
-	case (r.Spec.PgBouncer.AuthQuerySecret == nil || r.Spec.PgBouncer.AuthQuerySecret.Name == "") &&
-		r.Spec.PgBouncer.AuthQuery != "":
+	case !hasPGAuthentication && r.Spec.PgBouncer.AuthQuery != "":
 		result = append(result,
 			field.Invalid(
 				field.NewPath("spec", "pgbouncer", "authQuerySecret", "name"),
diff --git a/pkg/management/pgbouncer/config/config.go b/pkg/management/pgbouncer/config/config.go
index 7a2e3e1e..134108b7 100644
--- a/pkg/management/pgbouncer/config/config.go
+++ b/pkg/management/pgbouncer/config/config.go
@@ -42,11 +42,19 @@ const (
 
 	// ClientTLSCertPath is the path where the client TLS certificate
 	// is stored
-	clientTLSCertPath = ConfigsDir + "/server-tls/tls.crt"
+	clientTLSCertPath = ConfigsDir + "/client-tls/tls.crt"
 
 	// ClientTLSKeyPath is the path where the client TLS private key
 	// is stored
-	clientTLSKeyPath = ConfigsDir + "/server-tls/tls.key"
+	clientTLSKeyPath = ConfigsDir + "/client-tls/tls.key"
+
+	// ServerTLSCertPath is the path where the server TLS certificate
+	// is stored
+	serverTLSCertPath = ConfigsDir + "/server-tls/tls.crt"
+
+	// ServerTLSKeyPath is the path where the server TLS private key
+	// is stored
+	serverTLSKeyPath = ConfigsDir + "/server-tls/tls.key"
 
 	// ClientTLSCAPath is the path where the public key of the CA
 	// used to authenticate clients is stored
@@ -147,34 +155,41 @@ func BuildConfigurationFiles(pooler *apiv1.Pooler, secrets *Secrets) (Configurat
 
 	// if no user is provided we have to check the secret for a username, and we must be using basic auth
 	// if a user is provided it will overwrite the user in the secret, or we could be using cert auth
-	authQuerySecretType, err := detectSecretType(secrets.AuthQuery)
-	if err != nil {
-		return nil, fmt.Errorf("while detecting auth user secret type: %w", err)
+	authQuerySecret := secrets.AuthQuery
+	if authQuerySecret == nil {
+		authQuerySecret = secrets.ServerTLS
 	}
 
-	switch authQuerySecretType {
-	case corev1.SecretTypeBasicAuth:
-		authQueryUser = string(secrets.AuthQuery.Data["username"])
-		authQueryPassword = strings.ReplaceAll(string(secrets.AuthQuery.Data["password"]), "\"", "\"\"")
-
-	case corev1.SecretTypeTLS:
-		keyPair, err := certs.ParseServerSecret(secrets.AuthQuery)
+	if authQuerySecret != nil {
+		authQuerySecretType, err := detectSecretType(authQuerySecret)
 		if err != nil {
-			return nil, fmt.Errorf("while parsing TLS secret for auth user: %w", err)
+			return nil, fmt.Errorf("while detecting auth user secret type: %w", err)
 		}
 
-		certificate, err := keyPair.ParseCertificate()
-		if err != nil {
-			return nil, fmt.Errorf("while parsing certificate for auth user: %w", err)
+		switch authQuerySecretType {
+		case corev1.SecretTypeBasicAuth:
+			authQueryUser = string(authQuerySecret.Data["username"])
+			authQueryPassword = strings.ReplaceAll(string(authQuerySecret.Data["password"]), "\"", "\"\"")
+
+		case corev1.SecretTypeTLS:
+			keyPair, err := certs.ParseServerSecret(authQuerySecret)
+			if err != nil {
+				return nil, fmt.Errorf("while parsing TLS secret for auth user: %w", err)
+			}
+
+			certificate, err := keyPair.ParseCertificate()
+			if err != nil {
+				return nil, fmt.Errorf("while parsing certificate for auth user: %w", err)
+			}
+
+			authQueryUser = certificate.Subject.CommonName
+			isCertAuth = true
+			files[authUserCrtPath] = authQuerySecret.Data[certs.TLSCertKey]
+			files[authUserKeyPath] = authQuerySecret.Data[certs.TLSPrivateKeyKey]
+
+		default:
+			return nil, fmt.Errorf("unsupported secret type for auth query: %s", authQuerySecret.Type)
 		}
-
-		authQueryUser = certificate.Subject.CommonName
-		isCertAuth = true
-		files[authUserCrtPath] = secrets.AuthQuery.Data[certs.TLSCertKey]
-		files[authUserKeyPath] = secrets.AuthQuery.Data[certs.TLSPrivateKeyKey]
-
-	default:
-		return nil, fmt.Errorf("unsupported secret type for auth query: %s", secrets.AuthQuery.Type)
 	}
 
 	parameters := buildPgBouncerParameters(pooler.Spec.PgBouncer.Parameters)
@@ -186,6 +201,11 @@ func BuildConfigurationFiles(pooler *apiv1.Pooler, secrets *Secrets) (Configurat
 		parameters["auth_file"] = authFilePath
 	}
 
+	if secrets.ServerTLS != nil {
+		parameters["server_tls_cert_file"] = serverTLSCertPath
+		parameters["server_tls_key_file"] = serverTLSKeyPath
+	}
+
 	templateData := struct {
 		Pooler            *apiv1.Pooler
 		AuthQuery         string
@@ -211,22 +231,20 @@ func BuildConfigurationFiles(pooler *apiv1.Pooler, secrets *Secrets) (Configurat
 		PgHba:      pooler.Spec.PgBouncer.PgHBA,
 	}
 
-	err = pgBouncerIniTemplate.Execute(&pgbouncerIni, templateData)
-	if err != nil {
+	if err := pgBouncerIniTemplate.Execute(&pgbouncerIni, templateData); err != nil {
 		return nil, fmt.Errorf("while executing %s template: %w", PgBouncerIniFileName, err)
 	}
 	files[filepath.Join(ConfigsDir, PgBouncerIniFileName)] = pgbouncerIni.Bytes()
 
 	if !isCertAuth {
-		err = pgBouncerUserListTemplate.Execute(&pgbouncerUserList, templateData)
+		err := pgBouncerUserListTemplate.Execute(&pgbouncerUserList, templateData)
 		if err != nil {
 			return nil, fmt.Errorf("while executing %s template: %w", PgBouncerUserListFileName, err)
 		}
 		files[filepath.Join(ConfigsDir, PgBouncerUserListFileName)] = pgbouncerUserList.Bytes()
 	}
 
-	err = pgBouncerHBATemplate.Execute(&pgbouncerHBA, templateData)
-	if err != nil {
+	if err := pgBouncerHBATemplate.Execute(&pgbouncerHBA, templateData); err != nil {
 		return nil, fmt.Errorf("while executing %s template: %w", PgBouncerHBAConfFileName, err)
 	}
 	files[filepath.Join(ConfigsDir, PgBouncerHBAConfFileName)] = pgbouncerHBA.Bytes()
@@ -234,8 +252,13 @@ func BuildConfigurationFiles(pooler *apiv1.Pooler, secrets *Secrets) (Configurat
 	// The required crypto-material
 	files[serverTLSCAPath] = secrets.ServerCA.Data[certs.CACertKey]
 	files[clientTLSCAPath] = secrets.ClientCA.Data[certs.CACertKey]
-	files[clientTLSCertPath] = secrets.Client.Data[certs.TLSCertKey]
-	files[clientTLSKeyPath] = secrets.Client.Data[certs.TLSPrivateKeyKey]
+	files[clientTLSCertPath] = secrets.ClientTLS.Data[certs.TLSCertKey]
+	files[clientTLSKeyPath] = secrets.ClientTLS.Data[certs.TLSPrivateKeyKey]
+
+	if secrets.ServerTLS != nil {
+		files[serverTLSCertPath] = secrets.ServerTLS.Data[certs.TLSCertKey]
+		files[serverTLSKeyPath] = secrets.ServerTLS.Data[certs.TLSPrivateKeyKey]
+	}
 
 	return files, nil
 }
diff --git a/pkg/management/pgbouncer/config/data.go b/pkg/management/pgbouncer/config/data.go
index c0664f94..aad71cde 100644
--- a/pkg/management/pgbouncer/config/data.go
+++ b/pkg/management/pgbouncer/config/data.go
@@ -28,8 +28,12 @@ type Secrets struct {
 	// The secret containing the credentials to be used to execute the auth_query queries.
 	AuthQuery *corev1.Secret
 
+	// The secret containing the credentials for PgBouncer to authenticate
+	// against PostgreSQL server.
+	ServerTLS *corev1.Secret
+
 	// The TLS secret that will be used for client connections (application-side)
-	Client *corev1.Secret
+	ClientTLS *corev1.Secret
 
 	// The root-CA that will be used to validate client certificates
 	ClientCA *corev1.Secret
diff --git a/pkg/specs/pgbouncer/rbac.go b/pkg/specs/pgbouncer/rbac.go
index bfda6d75..12e15821 100644
--- a/pkg/specs/pgbouncer/rbac.go
+++ b/pkg/specs/pgbouncer/rbac.go
@@ -50,6 +50,10 @@ func Role(pooler *apiv1.Pooler) *rbacv1.Role {
 		if pooler.Status.Secrets.ClientCA.Name != "" {
 			secretNames = append(secretNames, pooler.Status.Secrets.ClientCA.Name)
 		}
+
+		if pooler.Status.Secrets.ClientTLS.Name != "" {
+			secretNames = append(secretNames, pooler.Status.Secrets.ClientTLS.Name)
+		}
 	}
 
 	return &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{



Test patch:
diff --git a/internal/controller/pooler_status_test.go b/internal/controller/pooler_status_test.go
index 955520af..4a807284 100644
--- a/internal/controller/pooler_status_test.go
+++ b/internal/controller/pooler_status_test.go
@@ -41,8 +41,12 @@ var _ = Describe("pooler_status unit tests", func() {
 
 	assertClusterInheritedStatus := func(pooler *apiv1.Pooler, cluster *apiv1.Cluster) {
 		Expect(pooler.Status.Secrets.ServerCA.Name).To(Equal(cluster.GetServerCASecretName()))
-		Expect(pooler.Status.Secrets.ServerTLS.Name).To(Equal(cluster.GetServerTLSSecretName()))
+		Expect(pooler.Status.Secrets.ServerTLS.Name).To(BeEmpty())
 		Expect(pooler.Status.Secrets.ClientCA.Name).To(Equal(cluster.GetClientCASecretName()))
+
+		// Yes, this may be confusing, but pgbouncer calls "client" its listening side, and
+		// we just use the same PostgreSQL certificates for that.
+		Expect(pooler.Status.Secrets.ClientTLS.Name).To(Equal(cluster.GetServerTLSSecretName()))
 	}
 	assertAuthUserStatus := func(pooler *apiv1.Pooler, authUserSecret *corev1.Secret) {
 		Expect(pooler.Status.Secrets.PgBouncerSecrets.AuthQuery.Name).To(Equal(authUserSecret.Name))
@@ -54,7 +58,25 @@ var _ = Describe("pooler_status unit tests", func() {
 		namespace := newFakeNamespace(env.client)
 		cluster := newFakeCNPGCluster(env.client, namespace)
 		pooler := newFakePooler(env.client, cluster)
-		res := &poolerManagedResources{Deployment: nil, Cluster: cluster}
+		res := &poolerManagedResources{
+			Deployment: nil,
+			Cluster:    cluster,
+			ClientCASecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetClientCASecretNameOrDefault(cluster),
+				},
+			},
+			ClientTLSSecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetClientTLSSecretNameOrDefault(cluster),
+				},
+			},
+			ServerCASecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetServerCASecretNameOrDefault(cluster),
+				},
+			},
+		}
 
 		err := env.poolerReconciler.updatePoolerStatus(ctx, pooler, res)
 		Expect(err).ToNot(HaveOccurred())
@@ -111,7 +133,26 @@ var _ = Describe("pooler_status unit tests", func() {
 		dep, err := pgbouncer.Deployment(pooler, cluster)
 		dep.Status.Replicas = *dep.Spec.Replicas
 		Expect(err).ToNot(HaveOccurred())
-		res := &poolerManagedResources{AuthUserSecret: authUserSecret, Cluster: cluster, Deployment: dep}
+		res := &poolerManagedResources{
+			AuthUserSecret: authUserSecret,
+			Cluster:        cluster,
+			Deployment:     dep,
+			ClientCASecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetClientCASecretNameOrDefault(cluster),
+				},
+			},
+			ClientTLSSecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetClientTLSSecretNameOrDefault(cluster),
+				},
+			},
+			ServerCASecret: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pooler.GetServerCASecretNameOrDefault(cluster),
+				},
+			},
+		}
 
 		By("making sure it updates the remote stored status when there are changes", func() {
 			poolerBefore := &apiv1.Pooler{}
diff --git a/internal/pgbouncer/management/controller/secrets_test.go b/internal/pgbouncer/management/controller/secrets_test.go
index 6f8d210b..98f35dd4 100644
--- a/internal/pgbouncer/management/controller/secrets_test.go
+++ b/internal/pgbouncer/management/controller/secrets_test.go
@@ -57,9 +57,9 @@ var _ = Describe("getSecrets tests", func() {
 
 			Expect(err).ToNot(HaveOccurred())
 			Expect(res.ClientCA.Name).To(Equal(clientCAName))
-			Expect(res.Client.Name).To(Equal(serverTLSName))
+			Expect(res.ClientTLS.Name).To(Equal(clientTLSName))
 			Expect(res.ServerCA.Name).To(Equal(serverCAName))
-			Expect(res.AuthQuery.Name).To(Equal(authQueryName))
+			Expect(res.AuthQuery).To(BeNil())
 		})
 	})
 
diff --git a/internal/pgbouncer/management/controller/suite_test.go b/internal/pgbouncer/management/controller/suite_test.go
index c2ec6959..e1bf97c8 100644
--- a/internal/pgbouncer/management/controller/suite_test.go
+++ b/internal/pgbouncer/management/controller/suite_test.go
@@ -36,6 +36,7 @@ import (
 
 const (
 	authQueryName = "authquery"
+	clientTLSName = "servertls"
 	serverCAName  = "serverca"
 	serverTLSName = "servertls"
 	clientCAName  = "clientca"
@@ -75,6 +76,7 @@ func buildTestEnv() (client.WithWatch, *apiv1.Pooler) {
 				ServerCA:  apiv1.SecretVersion{Name: serverCAName},
 				ServerTLS: apiv1.SecretVersion{Name: serverTLSName},
 				ClientCA:  apiv1.SecretVersion{Name: clientCAName},
+				ClientTLS: apiv1.SecretVersion{Name: clientTLSName},
 			},
 		},
 	}
diff --git a/tests/e2e/asserts_test.go b/tests/e2e/asserts_test.go
index 426fd339..07504bcb 100644
--- a/tests/e2e/asserts_test.go
+++ b/tests/e2e/asserts_test.go
@@ -1145,13 +1145,21 @@ func AssertDetachReplicaModeCluster(
 
 func AssertWritesToReplicaFails(
 	namespace, service, appDBName, appDBUser, appDBPass string,
+	connectionParams ...map[string]string,
 ) {
 	By(fmt.Sprintf("Verifying %v service doesn't allow writes", service), func() {
 		Eventually(func(g Gomega) {
 			forwardConn, conn, err := postgres.ForwardPSQLServiceConnection(
-				env.Ctx, env.Interface, env.RestClientConfig,
-				namespace, service,
-				appDBName, appDBUser, appDBPass)
+				env.Ctx,
+				env.Interface,
+				env.RestClientConfig,
+				namespace,
+				service,
+				appDBName,
+				appDBUser,
+				appDBPass,
+				connectionParams...,
+			)
 			defer func() {
 				_ = conn.Close()
 				forwardConn.Close()
@@ -1174,13 +1182,23 @@ func AssertWritesToReplicaFails(
 	})
 }
 
-func AssertWritesToPrimarySucceeds(namespace, service, appDBName, appDBUser, appDBPass string) {
+func AssertWritesToPrimarySucceeds(
+	namespace, service, appDBName, appDBUser, appDBPass string,
+	connectionParams ...map[string]string,
+) {
 	By(fmt.Sprintf("Verifying %v service correctly manages writes", service), func() {
 		Eventually(func(g Gomega) {
 			forwardConn, conn, err := postgres.ForwardPSQLServiceConnection(
-				env.Ctx, env.Interface, env.RestClientConfig,
-				namespace, service,
-				appDBName, appDBUser, appDBPass)
+				env.Ctx,
+				env.Interface,
+				env.RestClientConfig,
+				namespace,
+				service,
+				appDBName,
+				appDBUser,
+				appDBPass,
+				connectionParams...,
+			)
 			defer func() {
 				_ = conn.Close()
 				forwardConn.Close()
@@ -1482,9 +1500,11 @@ func CreateAndAssertServerCertificatesSecrets(
 	)
 	Expect(err).ToNot(HaveOccurred())
 
-	serverPair, err := caPair.CreateAndSignPair(cluster.GetServiceReadWriteName(), certs.CertTypeServer,
-		cluster.GetClusterAltDNSNames(),
-	)
+	altDNSNames := cluster.GetClusterAltDNSNames()
+	// Required to allow connecting via port-forwarding using "localhost" as the host
+	altDNSNames = append(altDNSNames, "localhost")
+
+	serverPair, err := caPair.CreateAndSignPair(cluster.GetServiceReadWriteName(), certs.CertTypeServer, altDNSNames)
 	Expect(err).ToNot(HaveOccurred())
 	serverSecret := serverPair.GenerateCertificateSecret(namespace, tlsSecName)
 	err = env.Client.Create(env.Ctx, serverSecret)
@@ -2093,6 +2113,7 @@ func assertReadWriteConnectionUsingPgBouncerService(
 	clusterName,
 	poolerYamlFilePath string,
 	isPoolerRW bool,
+	connectionParams ...map[string]string,
 ) {
 	poolerService, err := yaml.GetResourceNameFromYAML(env.Scheme, poolerYamlFilePath)
 	Expect(err).ToNot(HaveOccurred())
@@ -2101,16 +2122,15 @@ func assertReadWriteConnectionUsingPgBouncerService(
 		env.Ctx, env.Client,
 		clusterName, namespace, apiv1.ApplicationUserSecretSuffix)
 	Expect(err).ToNot(HaveOccurred())
-	AssertConnection(namespace, poolerService, postgres.AppDBName, appUser, generatedAppUserPassword, env)
 
 	// verify that, if pooler type setup read write then it will allow both read and
 	// write operations or if pooler type setup read only then it will allow only read operations
 	if isPoolerRW {
 		AssertWritesToPrimarySucceeds(namespace, poolerService, "app", appUser,
-			generatedAppUserPassword)
+			generatedAppUserPassword, connectionParams...)
 	} else {
 		AssertWritesToReplicaFails(namespace, poolerService, "app", appUser,
-			generatedAppUserPassword)
+			generatedAppUserPassword, connectionParams...)
 	}
 }
 
diff --git a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/cluster-user-supplied-client-server-certificates.yaml.template b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/cluster-user-supplied-certificates.yaml.template
similarity index 67%
rename from tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/cluster-user-supplied-client-server-certificates.yaml.template
rename to tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/cluster-user-supplied-certificates.yaml.template
index d6eef9d1..b8dba828 100644
--- a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/cluster-user-supplied-client-server-certificates.yaml.template
+++ b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/cluster-user-supplied-certificates.yaml.template
@@ -1,7 +1,7 @@
 apiVersion: postgresql.cnpg.io/v1
 kind: Cluster
 metadata:
-  name: pgbouncer-separate-client-server-ca
+  name: cluster-user-supplied-certificates
 spec:
   instances: 2
 
@@ -17,10 +17,15 @@ spec:
       log_replication_commands: 'on'
 
   certificates:
-    serverCASecret: my-postgresql-server-ca
-    serverTLSSecret: my-postgresql-server
-    clientCASecret: my-postgresql-client-ca
-    replicationTLSSecret: my-postgresql-client
+    serverTLSSecret: pg-server-cert
+    serverCASecret: pg-server-ca
+    clientCASecret: pg-client-ca
+    replicationTLSSecret: pg-client-streaming-replica
+
+  managed:
+    roles:
+      - name: pgbouncer
+        login: true
 
   bootstrap:
     initdb:
diff --git a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-ro.yaml b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-ro.yaml
similarity index 63%
rename from tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-ro.yaml
rename to tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-ro.yaml
index abecf184..b35d10aa 100644
--- a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-ro.yaml
+++ b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-ro.yaml
@@ -1,10 +1,10 @@
 apiVersion: postgresql.cnpg.io/v1
 kind: Pooler
 metadata:
-  name: pooler-connection-tls-ro
+  name: pooler-ro
 spec:
   cluster:
-    name: pgbouncer-separate-client-server-ca
+    name: cluster-user-supplied-certificates
 
   instances: 1
   type: ro
diff --git a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-rw.yaml b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-rw.yaml
similarity index 63%
rename from tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-rw.yaml
rename to tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-rw.yaml
index a167d72a..75f0db1c 100644
--- a/tests/e2e/fixtures/pgbouncer/pgbouncer_separate_client_server_ca/pgbouncer-pooler-tls-rw.yaml
+++ b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-rw.yaml
@@ -1,10 +1,10 @@
 apiVersion: postgresql.cnpg.io/v1
 kind: Pooler
 metadata:
-  name: pooler-connection-tls-rw
+  name: pooler-rw
 spec:
   cluster:
-    name: pgbouncer-separate-client-server-ca
+    name: cluster-user-supplied-certificates
 
   instances: 1
   type: rw
diff --git a/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-verify-full.yaml b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-verify-full.yaml
new file mode 100644
index 00000000..31adc458
--- /dev/null
+++ b/tests/e2e/fixtures/pgbouncer/pgbouncer_user_supplied_certificates/pgbouncer-verify-full.yaml
@@ -0,0 +1,29 @@
+apiVersion: postgresql.cnpg.io/v1
+kind: Pooler
+metadata:
+  name: pooler-verify-full
+spec:
+  cluster:
+    name: cluster-user-supplied-certificates
+
+  instances: 1
+  type: rw
+  pgbouncer:
+    poolMode: session
+    # for mTLS authentication, `authQuery` could be removed in a future version
+    # of pgbouncer
+    authQuery: "SELECT $1, NULL"
+    parameters:
+      server_tls_protocols: tlsv1.3
+      auth_type: cert
+      server_tls_sslmode: verify-full
+      client_tls_sslmode: verify-full
+
+    serverTLSSecret:
+      name: pooler-server-cert
+    serverCASecret:
+      name: pg-server-ca
+    clientCASecret:
+      name: pooler-client-ca
+    clientTLSSecret:
+      name: pooler-client-cert
diff --git a/tests/e2e/pgbouncer_test.go b/tests/e2e/pgbouncer_test.go
index 6631e151..527171d9 100644
--- a/tests/e2e/pgbouncer_test.go
+++ b/tests/e2e/pgbouncer_test.go
@@ -20,13 +20,23 @@ SPDX-License-Identifier: Apache-2.0
 package e2e
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/retry"
 	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
 
+	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/cloudnative-pg/cloudnative-pg/tests"
 	"github.com/cloudnative-pg/cloudnative-pg/tests/utils/clusterutils"
 	"github.com/cloudnative-pg/cloudnative-pg/tests/utils/exec"
+	testsUtils "github.com/cloudnative-pg/cloudnative-pg/tests/utils/postgres"
+	"github.com/cloudnative-pg/cloudnative-pg/tests/utils/secrets"
 	"github.com/cloudnative-pg/cloudnative-pg/tests/utils/yaml"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -146,50 +156,94 @@ var _ = Describe("PGBouncer Connections", Label(tests.LabelServiceConnectivity),
 		})
 	})
 
-	Context("user-defined certificates", func() {
-		It("can connect to Postgres via pgbouncer using different client and server CA", func() {
-			const (
-				folderPath                    = fixturesDir + "/pgbouncer/pgbouncer_separate_client_server_ca/"
-				sampleFileWithCertificate     = folderPath + "cluster-user-supplied-client-server-certificates.yaml.template"
-				poolerCertificateROSampleFile = folderPath + "pgbouncer-pooler-tls-ro.yaml"
-				poolerCertificateRWSampleFile = folderPath + "pgbouncer-pooler-tls-rw.yaml"
-				caSecName                     = "my-postgresql-server-ca"
-				tlsSecName                    = "my-postgresql-server"
-				tlsSecNameClient              = "my-postgresql-client"
-				caSecNameClient               = "my-postgresql-client-ca"
-			)
+	Context("user-defined certificates", Ordered, func() {
+		const (
+			folderPath    = fixturesDir + "/pgbouncer/pgbouncer_user_supplied_certificates/"
+			sampleCluster = folderPath + "cluster-user-supplied-certificates.yaml.template"
+		)
+		const (
+			// PostgreSQL certificates
+			postgresServerTLS      = "pg-server-cert"
+			postgresServerCA       = "pg-server-ca"
+			postgresClientCA       = "pg-client-ca"
+			postgresReplicationTLS = "pg-client-streaming-replica"
+		)
+
+		BeforeAll(func() {
 			// Create a cluster in a namespace that will be deleted after the test
 			namespace, err = env.CreateUniqueTestNamespace(env.Ctx, env.Client, "pgbouncer-separate-certificates")
 			Expect(err).ToNot(HaveOccurred())
-			clusterName, err = yaml.GetResourceNameFromYAML(env.Scheme, sampleFileWithCertificate)
+			clusterName, err = yaml.GetResourceNameFromYAML(env.Scheme, sampleCluster)
 			Expect(err).ToNot(HaveOccurred())
 
-			// Create certificates secret for server
-			CreateAndAssertServerCertificatesSecrets(namespace, clusterName,
-				caSecName, tlsSecName, true)
+			// Create client certificate secrets for PostgreSQL
+			CreateAndAssertServerCertificatesSecrets(namespace, clusterName, postgresServerCA, postgresServerTLS, true)
+			// Create server certificate secrets for PostgreSQL
+			CreateAndAssertClientCertificatesSecrets(namespace, clusterName, postgresClientCA, postgresReplicationTLS,
+				"app-user-cert", true)
 
-			// Create certificates secret for client
-			CreateAndAssertClientCertificatesSecrets(namespace, clusterName,
-				caSecNameClient, tlsSecNameClient, "app-user-cert", true)
+			AssertCreateCluster(namespace, clusterName, sampleCluster, env)
+		})
 
-			AssertCreateCluster(namespace, clusterName, sampleFileWithCertificate, env)
+		It("using automatic TLS configuration", func() {
+			const (
+				samplePoolerRO = folderPath + "pgbouncer-ro.yaml"
+				samplePoolerRW = folderPath + "pgbouncer-rw.yaml"
+			)
+			createAndAssertPgBouncerPoolerIsSetUp(namespace, samplePoolerRW, 1)
+			createAndAssertPgBouncerPoolerIsSetUp(namespace, samplePoolerRO, 1)
+			assertReadWriteConnectionUsingPgBouncerService(namespace, clusterName, samplePoolerRW, true)
+			assertReadWriteConnectionUsingPgBouncerService(namespace, clusterName, samplePoolerRO, false)
+		})
 
-			By("setting up read write type pgbouncer pooler", func() {
-				createAndAssertPgBouncerPoolerIsSetUp(namespace, poolerCertificateRWSampleFile, 1)
-			})
+		It("using manual TLS configuration (verify-full)", func() {
+			const (
+				samplePoolerVerifyFull = folderPath + "pgbouncer-verify-full.yaml"
+				// Pooler certificates
+				poolerServerTLS = "pooler-server-cert"
+				poolerServerCA  = postgresClientCA
+				poolerClientCA  = "pooler-client-ca"
+				poolerClientTLS = "pooler-client-cert"
+			)
 
-			By("setting up read only type pgbouncer pooler", func() {
-				createAndAssertPgBouncerPoolerIsSetUp(namespace, poolerCertificateROSampleFile, 1)
+			By("updating pg_hba and pg_ident of the Cluster to allow cert authentication for the app user", func() {
+				cluster := &apiv1.Cluster{}
+				err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
+					var err error
+					cluster, err = clusterutils.Get(env.Ctx, env.Client, namespace, clusterName)
+					Expect(err).ToNot(HaveOccurred())
+					cluster.Spec.PostgresConfiguration.PgHBA = []string{
+						"hostssl all app all cert map=pooler",
+						"hostssl all pgbouncer all cert",
+					}
+					cluster.Spec.PostgresConfiguration.PgIdent = []string{"pooler pgbouncer app"}
+					return env.Client.Update(env.Ctx, cluster)
+				})
+				Expect(err).ToNot(HaveOccurred())
 			})
 
-			By("verifying read and write connections using pgbouncer service", func() {
-				assertReadWriteConnectionUsingPgBouncerService(namespace, clusterName,
-					poolerCertificateRWSampleFile, true)
+			By("creating pooler and its certificates", func() {
+				poolerName, err := yaml.GetResourceNameFromYAML(env.Scheme, samplePoolerVerifyFull)
+				Expect(err).ToNot(HaveOccurred())
+				// Create server certificate secrets for Pooler
+				createPoolerServerCertificateSecret(namespace, poolerServerCA, poolerServerTLS)
+				// Create client certificate secrets for Pooler
+				createPoolerClientCertificateSecret(namespace, poolerName, poolerClientCA, poolerClientTLS, true)
+
+				createAndAssertPgBouncerPoolerIsSetUp(namespace, samplePoolerVerifyFull, 1)
 			})
 
-			By("verifying read connections using pgbouncer service", func() {
-				assertReadWriteConnectionUsingPgBouncerService(namespace, clusterName,
-					poolerCertificateROSampleFile, false)
+			By("connecting to the pooler using mTLS", func() {
+				caCertPath, clientCertPath, clientKeyPath := createAppClientCertificates(namespace, testsUtils.AppUser,
+					poolerClientCA)
+				connectionParams := map[string]string{
+					"sslkey":      clientKeyPath,
+					"sslcert":     clientCertPath,
+					"sslrootcert": caCertPath,
+					"sslmode":     "verify-full",
+				}
+				assertReadWriteConnectionUsingPgBouncerService(namespace, clusterName, samplePoolerVerifyFull,
+					true, connectionParams)
 			})
 		})
 	})
@@ -218,3 +272,81 @@ func runShowHelpInPod(pod *corev1.Pod) error {
 	)
 	return err
 }
+
+// createPoolerServerCertificateSecret fetches an existing PG server CA secret and creates a new certificate
+// secret for the pooler using that CA.
+// The pooler uses this certificate to authenticate with the PostgreSQL server.
+func createPoolerServerCertificateSecret(namespace, sourcePostgresServerCASecretName, secretName string) {
+	secret := &corev1.Secret{}
+	secretNamespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      sourcePostgresServerCASecretName,
+	}
+	err := env.Client.Get(env.Ctx, secretNamespacedName, secret)
+	Expect(err).ToNot(HaveOccurred())
+	caPair, err := certs.ParseCASecret(secret)
+	Expect(err).ToNot(HaveOccurred())
+
+	serverPair, err := caPair.CreateAndSignPair("pgbouncer", certs.CertTypeClient, nil)
+	Expect(err).ToNot(HaveOccurred())
+	serverSecret := serverPair.GenerateCertificateSecret(namespace, secretName)
+	err = env.Client.Create(env.Ctx, serverSecret)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+// createPoolerClientCertificateSecret creates a new CA secret and a new certificate
+// secret for the pooler.
+// The pooler uses this certificate to verify client connections to the pooler.
+func createPoolerClientCertificateSecret(
+	namespace, poolerName, caSecretName, tlsSecretName string, includeCAPrivateKey bool,
+) {
+	_, caPair, err := secrets.CreateSecretCA(
+		env.Ctx, env.Client,
+		namespace, poolerName, caSecretName, includeCAPrivateKey)
+	Expect(err).ToNot(HaveOccurred())
+
+	altDNSNames := []string{
+		"localhost", // Required to allow connecting via port-forwarding using "localhost" as the host
+		poolerName,
+		fmt.Sprintf("%v.%v", poolerName, namespace),
+		fmt.Sprintf("%v.%v.svc", poolerName, namespace),
+		fmt.Sprintf("%v.%v.svc.%s", poolerName, namespace, "cluster.local"),
+	}
+	clientPair, err := caPair.CreateAndSignPair(poolerName, certs.CertTypeServer, altDNSNames)
+	Expect(err).ToNot(HaveOccurred())
+	clientSecret := clientPair.GenerateCertificateSecret(namespace, tlsSecretName)
+	err = env.Client.Create(env.Ctx, clientSecret)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+// createAppClientCertificates fetches an existing pooler client CA secret and creates
+// certificates to allow connecting to the pooler from a client.
+// It returns the paths to the client CA certificate, client certificate and client key.
+func createAppClientCertificates(namespace, commonName, sourceCASecretName string,
+) (caCertPath, clientCertPath, clientKeyPath string) {
+	caSecret := &corev1.Secret{}
+	secretNamespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      sourceCASecretName,
+	}
+	err := env.Client.Get(env.Ctx, secretNamespacedName, caSecret)
+	Expect(err).ToNot(HaveOccurred())
+	caPair, err := certs.ParseCASecret(caSecret)
+	Expect(err).ToNot(HaveOccurred())
+
+	clientPair, err := caPair.CreateAndSignPair(commonName, certs.CertTypeClient, nil)
+	Expect(err).ToNot(HaveOccurred())
+
+	tmpDir := GinkgoT().TempDir()
+	caCert := filepath.Join(tmpDir, "ca.crt")
+	clientKey := filepath.Join(tmpDir, "client.key")
+	clientCert := filepath.Join(tmpDir, "client.crt")
+	err = os.WriteFile(caCert, caPair.Certificate, 0o600)
+	Expect(err).ToNot(HaveOccurred())
+	err = os.WriteFile(clientKey, clientPair.Private, 0o600)
+	Expect(err).ToNot(HaveOccurred())
+	err = os.WriteFile(clientCert, clientPair.Certificate, 0o600)
+	Expect(err).ToNot(HaveOccurred())
+
+	return caCert, clientCert, clientKey
+}
diff --git a/tests/utils/postgres/psql_connection.go b/tests/utils/postgres/psql_connection.go
index ecd79808..9911d57e 100644
--- a/tests/utils/postgres/psql_connection.go
+++ b/tests/utils/postgres/psql_connection.go
@@ -70,6 +70,7 @@ func startForwardConnection(
 	dbname,
 	userApp,
 	passApp string,
+	extraConnectionParams ...map[string]string,
 ) (*PSQLForwardConnection, *sql.DB, error) {
 	forwarder, err := forwardconnection.NewForwardConnection(
 		dialer,
@@ -91,7 +92,11 @@ func startForwardConnection(
 	}
 
 	connParameters := createConnectionParameters(userApp, passApp, localPort)
-
+	for _, params := range extraConnectionParams {
+		for key, value := range params {
+			connParameters[key] = value
+		}
+	}
 	pooler := pool.NewPgbouncerConnectionPool(configfile.CreateConnectionString(connParameters))
 
 	conn, err := pooler.Connection(dbname)
@@ -188,6 +193,7 @@ func ForwardPSQLServiceConnection(
 	dbname,
 	userApp,
 	passApp string,
+	extraConnectionParams ...map[string]string,
 ) (*PSQLForwardConnection, *sql.DB, error) {
 	dialer, portMap, err := forwardconnection.NewDialerFromService(
 		ctx,
@@ -200,7 +206,14 @@ func ForwardPSQLServiceConnection(
 		return nil, nil, err
 	}
 
-	psqlForwardConn, conn, err := startForwardConnection(dialer, portMap, dbname, userApp, passApp)
+	psqlForwardConn, conn, err := startForwardConnection(
+		dialer,
+		portMap,
+		dbname,
+		userApp,
+		passApp,
+		extraConnectionParams...,
+	)
 	if err != nil {
 		return nil, nil, err
 	}