Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: Pooler connection refused when upgrading to 1.28.0
### Is there an existing issue already for this bug?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new bug.

### I have read the troubleshooting guide

- [x] I have read the troubleshooting guide and I think this is a new bug.

### I am running a supported version of CloudNativePG

- [x] I have read the troubleshooting guide and I think this is a new bug.

### Contact Details

bck01215@gmail.com

### Version

1.28 (latest patch)

### What version of Kubernetes are you using?

1.34

### What is your Kubernetes environment?

Cloud: Amazon EKS

### How did you install the operator?

Helm

### What happened?

```
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  generation: 4
  name: pg-cluster-rw-pool
spec:
  cluster:
    name: pg-cluster
  instances: 3
  pgbouncer:
    parameters:
      default_pool_size: "100"
      max_client_conn: "1000"
    paused: false
    poolMode: transaction
  template:
    spec:
      containers:
      - name: pgbouncer
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
          requests:
            cpu: 10m
            memory: 10Mi
      initContainers:
      - name: bootstrap-controller
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
          requests:
            cpu: 10m
            memory: 10Mi
  type: rw
```

I applied the update to 1.28.0 with argo and began getting connection refused errors in the logs


### Cluster resource

```shell
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  generation: 16
  name: pg-cluster
  namespace: cluster-dev
spec:
  affinity:
    enablePodAntiAffinity: true
    podAntiAffinityType: preferred
    topologyKey: topology.ebs.csi.aws.com/zone
  bootstrap:
    recovery:
      database: app
      owner: app
      source: pg-cluster
  enablePDB: true
  enableSuperuserAccess: true
  externalClusters:
  - barmanObjectStore:
      destinationPath: s3://pulse-fm-prod-postgres/pg-cluster/
      s3Credentials:
        inheritFromIAMRole: true
        region:
          key: region
          name: fm-timescale-aws-region
      wal:
        compression: bzip2
        maxParallel: 8
    name: pg-cluster
  failoverDelay: 0
  imageName: redacted.dkr.ecr.us-east-1.amazonaws.com/cnpg:18-0-ts2-23-0 # cnpg/timescaledb image
  instances: 2
  logLevel: info
  managed:
    services:
      additional:
      - selectorType: rw
        serviceTemplate:
          metadata:
            annotations:
              ad.datadoghq.com/service.checks: |
                {
                  "postgres": {
                    "init_config": {},
                    "instances": [
                      {
                        "host": "%%host%%",
                        "reported_hostname": "fm-dev",
                        "port":"5432",
                        "username":"datadog",
                        "password":"ENC[k8s_secret@default/dd-tools-pass/dd-tools-pass]",
                        "dbname": "fm",
                        "dbm": "true",
                        "collect_schemas": {
                          "enabled": true
                        },
                        "relations": [
                          {"relation_regex": ".*"}
                        ],
                        "tags": ["team:tools", "env:dev", "postgres_cluster:tools"]
                      }
                    ]
                  }
                }
              external-dns.alpha.kubernetes.io/hostname: tools.default.db.tfm-dev.internal
              service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags: Usage=dev,Product=fm
              service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
              service.beta.kubernetes.io/aws-load-balancer-internal: "true"
              service.beta.kubernetes.io/aws-load-balancer-scheme: internal
              service.beta.kubernetes.io/aws-load-balancer-type: nlb
            name: cluster-external-rw
          spec:
            type: LoadBalancer
        updateStrategy: patch
      - selectorType: ro
        serviceTemplate:
          metadata:
            annotations:
              external-dns.alpha.kubernetes.io/hostname: tools-repl.default.db.tfm-dev.internal
              service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags: Usage=dev,Product=fm
              service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
              service.beta.kubernetes.io/aws-load-balancer-internal: "true"
              service.beta.kubernetes.io/aws-load-balancer-scheme: internal
              service.beta.kubernetes.io/aws-load-balancer-type: nlb
            name: cluster-external-ro
          spec:
            type: LoadBalancer
        updateStrategy: patch
  maxSyncReplicas: 0
  minSyncReplicas: 0
  monitoring:
    customQueriesConfigMap:
    - key: queries
      name: cnpg-default-monitoring
    disableDefaultQueries: false
    enablePodMonitor: true
  plugins:
  - enabled: true
    isWALArchiver: true
    name: barman-cloud.cloudnative-pg.io
    parameters:
      barmanObjectName: pg-cluster
      serverName: pg-cluster
  postgresGID: 26
  postgresUID: 26
  postgresql:
    parameters:
      archive_mode: "on"
      archive_timeout: 5min
      dynamic_shared_memory_type: posix
      full_page_writes: "on"
      log_destination: csvlog
      log_directory: /controller/log
      log_filename: postgres
      log_rotation_age: "0"
      log_rotation_size: "0"
      log_truncate_on_rotation: "false"
      logging_collector: "on"
      max_connections: "300"
      max_locks_per_transaction: "128"
      max_parallel_workers: "32"
      max_replication_slots: "32"
      max_standby_archive_delay: 900s
      max_standby_streaming_delay: 900s
      max_worker_processes: "32"
      password_encryption: scram-sha-256
      pg_stat_statements.track: ALL
      pg_stat_statements.track_utility: "off"
      shared_memory_type: mmap
      shared_preload_libraries: ""
      ssl_max_protocol_version: TLSv1.3
      ssl_min_protocol_version: TLSv1.3
      timescaledb.license_key: CommunityLicense
      track_activity_query_size: "4096"
      wal_keep_size: 512MB
      wal_level: logical
      wal_log_hints: "on"
      wal_receiver_timeout: 5s
      wal_sender_timeout: 5s
    shared_preload_libraries:
    - timescaledb
    - pg_stat_statements
    - set_user
    - pg_stat_kcache
    syncReplicaElectionConstraint:
      enabled: false
  primaryUpdateMethod: restart
  primaryUpdateStrategy: unsupervised
  probes:
    liveness:
      isolationCheck:
        connectionTimeout: 1000
        enabled: true
        requestTimeout: 1000
  replicationSlots:
    highAvailability:
      enabled: true
      slotPrefix: _cnpg_
    synchronizeReplicas:
      enabled: true
    updateInterval: 30
  resources:
    limits:
      cpu: "4"
      memory: 4Gi
    requests:
      cpu: 500m
      memory: 500Mi
  smartShutdownTimeout: 180
  startDelay: 3600
  stopDelay: 1800
  storage:
    resizeInUseVolumes: true
    size: 100Gi
    storageClass: gp3
  switchoverDelay: 3600
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.ebs.csi.aws.com/zone
    whenUnsatisfiable: DoNotSchedule
status:
  availableArchitectures:
  - goArch: amd64
    hash: 527e2e3b680dfba7f7578b98530f755f36156e7be00acda3318ef36fe4f4418f
  - goArch: arm64
    hash: aaa74c6061f3fe30f230a664b4f6076bb886c9a89d4eba3293483525c5cff533
  certificates:
    clientCASecret: pg-cluster-ca
    expirations:
      pg-cluster-ca: 2026-02-02 01:17:35 +0000 UTC
      pg-cluster-replication: 2026-02-02 01:17:35 +0000 UTC
      pg-cluster-server: 2026-02-18 00:18:39 +0000 UTC
    replicationTLSSecret: pg-cluster-replication
    serverAltDNSNames:
    - pg-cluster-rw
    - pg-cluster-rw.cluster-dev
    - pg-cluster-rw.cluster-dev.svc
    - pg-cluster-rw.cluster-dev.svc.cluster.local
    - pg-cluster-r
    - pg-cluster-r.cluster-dev
    - pg-cluster-r.cluster-dev.svc
    - pg-cluster-r.cluster-dev.svc.cluster.local
    - pg-cluster-ro
    - pg-cluster-ro.cluster-dev
    - pg-cluster-ro.cluster-dev.svc
    - pg-cluster-ro.cluster-dev.svc.cluster.local
    - cluster-external-rw
    - cluster-external-rw.cluster-dev
    - cluster-external-rw.cluster-dev.svc
    - cluster-external-rw.cluster-dev.svc.cluster.local
    - cluster-external-ro
    - cluster-external-ro.cluster-dev
    - cluster-external-ro.cluster-dev.svc
    - cluster-external-ro.cluster-dev.svc.cluster.local
    serverCASecret: pg-cluster-ca
    serverTLSSecret: pg-cluster-server
  cloudNativePGCommitHash: a9696201f
  cloudNativePGOperatorHash: 527e2e3b680dfba7f7578b98530f755f36156e7be00acda3318ef36fe4f4418f
  conditions:
  - lastTransitionTime: "2025-12-02T17:17:43Z"
    message: A single, unique system ID was found across reporting instances.
    reason: Unique
    status: "True"
    type: ConsistentSystemID
  - lastTransitionTime: "2025-12-09T18:26:16Z"
    message: Cluster is Ready
    reason: ClusterIsReady
    status: "True"
    type: Ready
  - lastTransitionTime: "2025-12-09T18:24:43Z"
    message: Continuous archiving is working
    reason: ContinuousArchivingSuccess
    status: "True"
    type: ContinuousArchiving
  - lastTransitionTime: "2025-12-09T00:02:01Z"
    message: Backup was successful
    reason: LastBackupSucceeded
    status: "True"
    type: LastBackupSucceeded
  configMapResourceVersion:
    metrics:
      cnpg-default-monitoring: "359312045"
  currentPrimary: pg-cluster-4
  currentPrimaryTimestamp: "2025-12-09T18:24:38.717762Z"
  healthyPVC:
  - pg-cluster-4
  - pg-cluster-6
  image: redacted.dkr.ecr.us-east-1.amazonaws.com/cnpg:18-0-ts2-23-0
  instanceNames:
  - pg-cluster-4
  - pg-cluster-6
  instances: 2
  instancesReportedState:
    pg-cluster-4:
      ip: 10.4.6.97
      isPrimary: true
      timeLineID: 66
    pg-cluster-6:
      ip: 10.4.0.121
      isPrimary: false
      timeLineID: 66
  instancesStatus:
    healthy:
    - pg-cluster-4
    - pg-cluster-6
  latestGeneratedNode: 6
  managedRolesStatus: {}
  pgDataImageInfo:
    image: redacted.dkr.ecr.us-east-1.amazonaws.com/cnpg:18-0-ts2-23-0
    majorVersion: 18
  phase: Cluster in healthy state
  pluginStatus:
  - capabilities:
    - TYPE_RECONCILER_HOOKS
    - TYPE_LIFECYCLE_SERVICE
    name: barman-cloud.cloudnative-pg.io
    version: 0.9.0
  poolerIntegrations:
    pgBouncerIntegration:
      secrets:
      - pg-cluster-pooler
  pvcCount: 2
  readService: pg-cluster-r
  readyInstances: 2
  secretsResourceVersion:
    applicationSecretVersion: "386344441"
    clientCaSecretVersion: "359312014"
    replicationSecretVersion: "359312016"
    serverCaSecretVersion: "359312014"
    serverSecretVersion: "370780410"
    superuserSecretVersion: "386344440"
  switchReplicaClusterStatus: {}
  systemID: "7569348818666958871"
  targetPrimary: pg-cluster-4
  targetPrimaryTimestamp: "2025-12-09T18:24:22.985685Z"
  timelineID: 66
  topology:
    instances:
      pg-cluster-4: {}
      pg-cluster-6: {}
    nodesUsed: 2
    successfullyExtracted: true
  writeService: pg-cluster-rw
```

### Relevant log output

```shell
{"level":"info","ts":"2025-12-09T18:58:25.245511759Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:25.245 UTC","pid":"12","level":"LOG","msg":"kernel file descriptor limit: 65536 (hard: 1048576); max_client_conn: 1000, max expected fd use: 1012"}}
{"level":"info","ts":"2025-12-09T18:58:25.245962782Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:25.245 UTC","pid":"12","level":"LOG","msg":"listening on 0.0.0.0:5432"}}
{"level":"info","ts":"2025-12-09T18:58:25.246037299Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:25.245 UTC","pid":"12","level":"LOG","msg":"listening on [::]:5432"}}
{"level":"info","ts":"2025-12-09T18:58:25.246104012Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:25.246 UTC","pid":"12","level":"LOG","msg":"listening on unix:/controller/run/.s.PGSQL.5432"}}
{"level":"info","ts":"2025-12-09T18:58:25.246147311Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:25.246 UTC","pid":"12","level":"LOG","msg":"process up: PgBouncer 1.25.1, libevent 2.1.12-stable (epoll), adns: c-ares 1.34.5, tls: OpenSSL 3.5.4 30 Sep 2025"}}
{"level":"info","ts":"2025-12-09T18:58:45.460643275Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.460 UTC","pid":"12","level":"LOG","msg":"C-0x557233534560: (nodb)/(nouser)@10.4.0.55:35668 registered new auto-database: fm"}}
{"level":"info","ts":"2025-12-09T18:58:45.460751086Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.460 UTC","pid":"12","level":"LOG","msg":"C-0x557233534560: (nodb)/(nouser)@10.4.0.55:35668 registered new auto-database: postgres"}}
{"level":"info","ts":"2025-12-09T18:58:45.461816124Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.461 UTC","pid":"12","level":"LOG","msg":"S-0x5572335633e0: postgres/pg-cluster-rw@172.20.129.144:5432 new connection to server (from 10.4.6.47:38722)"}}
{"level":"info","ts":"2025-12-09T18:58:45.466677629Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.466 UTC","pid":"12","level":"LOG","msg":"S-0x5572335633e0: postgres/pg-cluster-rw@172.20.129.144:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384"}}
{"level":"info","ts":"2025-12-09T18:58:45.46784964Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.467 UTC","pid":"12","level":"WARNING","msg":"tls_sbufio_recv: read failed: error:0A000413:SSL routines::ssl/tls alert unsupported certificate"}}
{"level":"info","ts":"2025-12-09T18:58:45.467884738Z","msg":"record","logger":"pgbouncer-manager","pipe":"stderr","record":{"timestamp":"2025-12-09 18:58:45.467 UTC","pid":"12","level":"LOG","msg":"S-0x5572335633e0: postgres/pg-cluster-rw@172.20.129.144:5432 closing because: server conn crashed? (age=0s)"}}
```

### Code of Conduct

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

Hints from issue comments:
@dosubot: <!-- Answer -->
Your Pooler connection refused and SSL certificate errors after upgrading to CloudNativePG 1.28.0 are most likely caused by a mismatch or incompatibility in the TLS certificates used by PgBouncer and PostgreSQL, or by permission issues with the `user_search` function and the `cnpg_pooler_pgbouncer` user.

CloudNativePG 1.28.0 introduced stricter TLS handling and the ability to specify custom TLS secrets for PgBouncer. If the certificates or CA data used by PgBouncer don't match what PostgreSQL expects, you'll see errors like `tls_sbufio_recv: read failed: error:0A000413:SSL routines::ssl/tls alert unsupported certificate` and connection refusals. This can happen if secrets were changed, not updated, or if you are reusing old secrets after the upgrade. PgBouncer 1.25.1 (the new default) also enforces stricter TLS checks and may surface issues that were previously ignored [[release notes](https://github.com/cloudnative-pg/cloudnative-pg/blob/a9696201f760013182c6cdba7c4ed3c236a6423b/docs/src/release_notes/v1.28.md#L41-L128)].

Another common cause is a permissions or ownership problem with the `user_search` function or the `cnpg_pooler_pgbouncer` user. After upgrades or when custom secrets are used, you may need to manually recreate the `user_search` function as the `postgres` superuser and grant EXECUTE to the pooler user in each database you want to access [[example](https://github.com/cloudnative-pg/cloudnative-pg/issues/8165), [example](https://github.com/cloudnative-pg/cloudnative-pg/issues/6262)].

**Troubleshooting steps:**

1. Check that the Pooler and Cluster are using the same CA and server certificates. If you are using custom secrets, make sure they are in the correct format and up to date. If not, let the operator manage the secrets automatically.
2. In each application database, as the `postgres` superuser, run:
   ```sql
   DROP FUNCTION IF EXISTS public.user_search(text);
   CREATE OR REPLACE FUNCTION public.user_search(uname TEXT)
     RETURNS TABLE (usename name, passwd text)
     LANGUAGE sql SECURITY DEFINER AS
     'SELECT usename, passwd FROM pg_catalog.pg_shadow WHERE usename=$1;';
   REVOKE ALL ON FUNCTION public.user_search(text) FROM public;
   GRANT EXECUTE ON FUNCTION public.user_search(text) TO cnpg_pooler_pgbouncer;
   ALTER FUNCTION public.user_search(text) OWNER TO postgres;
   ```
3. Make sure the `cnpg_pooler_pgbouncer` user exists and has CONNECT on all relevant databases.
4. If you changed any secrets or fixed permissions, restart the Pooler pods to ensure they pick up the changes.
5. If the issue persists, increase PgBouncer log verbosity and check for more detailed errors.

These steps resolve most Pooler connection and certificate issues after upgrades or configuration changes [[docs](https://cloudnative-pg.io/documentation/current/connection_pooling/)].


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8d00b4be-221d-4573-9dd3-51c0cabe38d5?feedback_type=other)</sup>

[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/89b818f3-8b5e-449b-bfb3-64a9360d9665/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Your%20Pooler%20connection%20refused%20and%20SSL%20certificate%20errors%20after%20upgrading%20to%20CloudNativePG%201.28.0%20are%20most%20likely%20caused%20by%20a%20mismatch%20or%20incompatibility%20in%20the%20TLS%20certificates%20used%20by%20PgBouncer%20and%20PostgreSQL%2C%20or%20by%20permission%20issues%20with%20the%20%60user_search%60%20function%20and%20the%20%60cnpg_pooler_pgbouncer%60%20user.%0A%0ACloudNativePG%201.28.0%20introduced%20stricter%20TLS%20handling%20and%20the%20ability%20to%20specify%20custom%20TLS%20secrets%20for%20PgBouncer.%20If%20the%20certificates%20or%20CA%20data%20used%20by%20PgBouncer%20don%27t%20match%20what%20PostgreSQL%20expects%2C%20you%27ll%20see%20errors%20like%20%60tls_sbufio_recv%3A%20read%20failed%3A%20error%3A0A000413%3ASSL%20routines%3A%3Assl/tls%20alert%20unsupported%20certificate%60%20and%20connection%20refusals.%20This%20can%20happen%20if%20secrets%20were%20changed%2C%20not%20updated%2C%20or%20if%20you%20are%20reusing%20old%20secrets%20after%20the%20upgrade.%20PgBouncer%201.25.1%20%28the%20new%20default%29%20also%20enforces%20stricter%20TLS%20checks%20and%20may%20surface%20issues%20that%20were%20previously%20ignored%20%5B%5Brelease%20notes%5D%28https%3A//github.com/cloudnative-pg/cloudnative-pg/blob/a9696201f760013182c6cdba7c4ed3c236a6423b/docs/src/release_notes/v1.28.md%23L41-L128%29%5D.%0A%0AAnother%20common%20cause%20is%20a%20permissions%20or%20ownership%20problem%20with%20the%20%60user_search%60%20function%20or%20the%20%60cnpg_pooler_pgbouncer%60%20user.%20After%20upgrades%20or%20when%20custom%20secrets%20are%20used%2C%20you%20may%20need%20to%20manually%20recreate%20the%20%60user_search%60%20function%20as%20the%20%60postgres%60%20superuser%20and%20grant%20EXECUTE%20to%20the%20pooler%20user%20in%20each%20database%20you%20want%20to%20access%20%5B%5Bexample%5D%28https%3A//github.com/cloudnative-pg/cloudnative-pg/issues/8165%29%2C%20%5Bexample%5D%28https%3A//github.com/cloudnative-pg/cloudnative-pg/issues/6262%29%5D.%0A%0A%2A%2ATroubleshooting%20steps%3A%2A%2A%0A%0A1.%20Check%20that%20the%20Pooler%20and%20Cluster%20are%20using%20the%20same%20CA%20and%20server%20certificates.%20If%20you%20are%20using%20custom%20secrets%2C%20make%20sure%20they%20are%20in%20the%20correct%20format%20and%20up%20to%20date.%20If%20not%2C%20let%20the%20operator%20manage%20the%20secrets%20automatically.%0A2.%20In%20each%20application%20database%2C%20as%20the%20%60postgres%60%20superuser%2C%20run%3A%0A%20%20%20%60%60%60sql%0A%20%20%20DROP%20FUNCTION%20IF%20EXISTS%20public.user_search%28text%29%3B%0A%20%20%20CREATE%20OR%20REPLACE%20FUNCTION%20public.user_search%28uname%20TEXT%29%0A%20%20%20%20%20RETURNS%20TABLE%20%28usename%20name%2C%20passwd%20text%29%0A%20%20%20%20%20LANGUAGE%20sql%20SECURITY%20DEFINER%20AS%0A%20%20%20%20%20%27SELECT%20usename%2C%20passwd%20FROM%20pg_catalog.pg_shadow%20WHERE%20usename%3D%241%3B%27%3B%0A%20%20%20REVOKE%20ALL%20ON%20FUNCTION%20public.user_search%28text%29%20FROM%20public%3B%0A%20%20%20GRANT%20EXECUTE%20ON%20FUNCTION%20public.user_search%28text%29%20TO%20cnpg_pooler_pgbouncer%3B%0A%20%20%20ALTER%20FUNCTION%20public.user_search%28text%29%20OWNER%20TO%20postgres%3B%0A%20%20%20%60%60%60%0A3.%20Make%20sure%20the%20%60cnpg_pooler_pgbouncer%60%20user%20exists%20and%20has%20CONNECT%20on%20all%20relevant%20databases.%0A4.%20If%20you%20changed%20any%20secrets%20or%20fixed%20permissions%2C%20restart%20the%20Pooler%20pods%20to%20ensure%20they%20pick%20up%20the%20changes.%0A5.%20If%20the%20issue%20persists%2C%20increase%20PgBouncer%20log%20verbosity%20and%20check%20for%20more%20detailed%20errors.%0A%0AThese%20steps%20resolve%20most%20Pooler%20connection%20and%20certificate%20issues%20after%20upgrades%20or%20configuration%20changes%20%5B%5Bdocs%5D%28https%3A//cloudnative-pg.io/documentation/current/connection_pooling/%29%5D.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/cloudnative-pg/cloudnative-pg/issues/9392)@Ganonmaster: I'm also running into this. I'm letting the operator manage my certificates and secrets.

Edit: deleting the Pooler and its pods, and then readding the Pooler fixed it for me. I'm assuming the certs were re-initialized and just didn't get taken up in the upgrade.@jsimcina: > I'm also running into this. I'm letting the operator manage my certificates and secrets.
> 
> Edit: deleting the Pooler and its pods, and then readding the Pooler fixed it for me. I'm assuming the certs were re-initialized and just didn't get taken up in the upgrade.

Can confirm deleting the pooler and redeploying it worked for me as well@MatthewStockdale: Same issue, same fix.@mnencia: Found the issue! In v1.28, the meaning of the `status.secrets.serverTLS` field has slightly changed; now it should be empty by default, and the old value is causing pgbouncer to use the wrong certificate for PostgreSQL authentication.

Can you try this workaround to clear the stale status?

```bash
# Clear the stale ServerTLS status field
kubectl patch pooler -n <your_namespace> <pooler_name> --type=json \
  -p='[{"op": "remove", "path": "/status/secrets/serverTLS"}]' --subresource=status

# Restart the pooler deployment to pick up the corrected configuration
kubectl rollout restart deployment -n <your_namespace> <pooler_name>
```

The operator should reconcile and fix the configuration. Please let me know if this resolves the issue - I have a fix ready to submit.
@diogotorres97: Hello,

Deleting and re-adding the poolers didn't work here neither the patch.
```

{"level":"info","ts":"2025-12-11T10:26:29.018653115Z","msg":"Starting CloudNativePG PgBouncer Instance Manager","logger":"pgbouncer-manager","version":"1.28.0","build":{"Version":"1.28.0","Commit":"a9696201f","Date":"2025-12-09"}}            │
│ {"level":"error","ts":"2025-12-11T10:26:29.124393327Z","msg":"Error while running manager","logger":"pgbouncer-manager","error":"while initializing reconciler: while reading secrets: while getting client TLS secret: resource name may not be  │
│ empty","stacktrace":"github.com/cloudnative-pg/machinery/pkg/log.(*logger).Error\n\tpkg/mod/github.com/cloudnative-pg/machinery@v0.3.1/pkg/log/log.go:125\ngithub.com/cloudnative-pg/cloudnative-pg/internal/cmd/manager/pgbouncer/run.NewCmd.fun │
│ c2\n\tinternal/cmd/manager/pgbouncer/run/cmd.go:78\ngithub.com/spf13/cobra.(*Command).execute\n\tpkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1015\ngithub.com/spf13/cobra.(*Command).ExecuteC\n\tpkg/mod/github.com/spf13/cobra@v1.10.1/com │
│ mand.go:1148\ngithub.com/spf13/cobra.(*Command).Execute\n\tpkg/mod/github.com/spf13/cobra@v1.10.1/command.go:1071\nmain.main\n\tcmd/manager/main.go:71\nruntime.main\n\t/opt/hostedtoolcache/go/1.25.5/x64/src/runtime/proc.go:285"}
```@mnencia: @diogotorres97 can you please share your Cluster and Pooler manifests?

Solution patch:
diff --git a/internal/controller/pooler_status.go b/internal/controller/pooler_status.go
index dd362475..492f7b69 100644
--- a/internal/controller/pooler_status.go
+++ b/internal/controller/pooler_status.go
@@ -54,6 +54,8 @@ func (r *PoolerReconciler) updatePoolerStatus(
 			Name:    resources.ServerCASecret.Name,
 			Version: resources.ServerCASecret.ResourceVersion,
 		}
+	} else {
+		updatedStatus.Secrets.ServerCA = apiv1.SecretVersion{}
 	}
 
 	if resources.ClientCASecret != nil {
@@ -61,6 +63,8 @@ func (r *PoolerReconciler) updatePoolerStatus(
 			Name:    resources.ClientCASecret.Name,
 			Version: resources.ClientCASecret.ResourceVersion,
 		}
+	} else {
+		updatedStatus.Secrets.ClientCA = apiv1.SecretVersion{}
 	}
 
 	if resources.ClientTLSSecret != nil {
@@ -68,6 +72,8 @@ func (r *PoolerReconciler) updatePoolerStatus(
 			Name:    resources.ClientTLSSecret.Name,
 			Version: resources.ClientTLSSecret.ResourceVersion,
 		}
+	} else {
+		updatedStatus.Secrets.ClientTLS = apiv1.SecretVersion{}
 	}
 
 	if resources.ServerTLSSecret != nil {
@@ -75,6 +81,11 @@ func (r *PoolerReconciler) updatePoolerStatus(
 			Name:    resources.ServerTLSSecret.Name,
 			Version: resources.ServerTLSSecret.ResourceVersion,
 		}
+	} else {
+		// Clear ServerTLS when not using manual TLS authentication.
+		// This is particularly important for migration from v1.27, where
+		// ServerTLS was always set to the cluster's server certificate.
+		updatedStatus.Secrets.ServerTLS = apiv1.SecretVersion{}
 	}
 
 	if resources.Deployment != nil {



Test patch:
diff --git a/internal/controller/pooler_status_test.go b/internal/controller/pooler_status_test.go
index 4a807284..3dc4b87b 100644
--- a/internal/controller/pooler_status_test.go
+++ b/internal/controller/pooler_status_test.go
@@ -186,4 +186,47 @@ var _ = Describe("pooler_status unit tests", func() {
 			Expect(poolerBefore.Status).To(BeEquivalentTo(poolerAfter.Status))
 		})
 	})
+
+	It("should clear ServerTLS status when not using manual TLS authentication (migration to v1.28)", func() {
+		ctx := context.Background()
+		namespace := newFakeNamespace(env.client)
+		cluster := newFakeCNPGCluster(env.client, namespace)
+		pooler := newFakePooler(env.client, cluster)
+
+		// Simulate a pre v1.28 Pooler with ServerTLS populated
+		pooler.Status.Secrets = &apiv1.PoolerSecrets{
+			ServerTLS: apiv1.SecretVersion{
+				Name:    cluster.GetServerTLSSecretName(),
+				Version: "stale-version-from-v127",
+			},
+		}
+
+		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),
+				},
+			},
+			ServerTLSSecret: nil,
+		}
+
+		err := env.poolerReconciler.updatePoolerStatus(ctx, pooler, res)
+		Expect(err).ToNot(HaveOccurred())
+
+		Expect(pooler.Status.Secrets.ServerTLS.Name).To(BeEmpty())
+		Expect(pooler.Status.Secrets.ServerTLS.Version).To(BeEmpty())
+		assertClusterInheritedStatus(pooler, cluster)
+	})
 })
diff --git a/tests/e2e/asserts_test.go b/tests/e2e/asserts_test.go
index 9b487856..df2495c6 100644
--- a/tests/e2e/asserts_test.go
+++ b/tests/e2e/asserts_test.go
@@ -2123,13 +2123,22 @@ func assertReadWriteConnectionUsingPgBouncerService(
 		clusterName, namespace, apiv1.ApplicationUserSecretSuffix)
 	Expect(err).ToNot(HaveOccurred())
 
+	// Get the database name from the cluster configuration
+	cluster, err := clusterutils.Get(env.Ctx, env.Client, namespace, clusterName)
+	Expect(err).ToNot(HaveOccurred())
+
+	dbName := cluster.GetApplicationDatabaseName()
+	if dbName == "" {
+		dbName = apiv1.DefaultApplicationDatabaseName
+	}
+
 	// 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,
+		AssertWritesToPrimarySucceeds(namespace, poolerService, dbName, appUser,
 			generatedAppUserPassword, connectionParams...)
 	} else {
-		AssertWritesToReplicaFails(namespace, poolerService, "app", appUser,
+		AssertWritesToReplicaFails(namespace, poolerService, dbName, appUser,
 			generatedAppUserPassword, connectionParams...)
 	}
 }
diff --git a/tests/e2e/upgrade_test.go b/tests/e2e/upgrade_test.go
index 7bd70acf..405a41a5 100644
--- a/tests/e2e/upgrade_test.go
+++ b/tests/e2e/upgrade_test.go
@@ -674,6 +674,10 @@ var _ = Describe("Upgrade", Label(tests.LabelUpgrade, tests.LabelNoOpenshift), O
 
 		AssertConfUpgrade(clusterName1, upgradeNamespace)
 
+		By("verifying connection through pooler after upgrade", func() {
+			assertReadWriteConnectionUsingPgBouncerService(upgradeNamespace, clusterName1, pgBouncerSampleFile, true)
+		})
+
 		By("installing a second Cluster on the upgraded operator", func() {
 			// set the serverName to a random name
 			err := os.Setenv("SERVER_NAME", serverName2)