sekskant Blog

OVHcloud + Managed PostgreSQL: Correct certificate validation with node "pg" module and payload

Intro#

Many managed database services, like the one from OVHcloud, offer TLS-secured connections to your database. But do you understand whether that actually makes you secure in your context, or whether you are still a good target for man-in-the-middle attacks? This article shares some insights from my own experience.

The issue#

sslmode=require: warnings and errors#

When you use, for example, OVHcloud, you can copy+paste the connection string from their UI and drop it into your application. The one from OVHcloud, again like many others, contains the parameter “sslmode=require”, but this is not enough to protect you against a man-in-the-middle attack. It only protects you from passive sniffing on your untrusted network. It is essential, but the job is still not done.

If you use that string in a stack that depends on node-postgres/pg, it is likely you run into this warning:

In the next major version (pg-connection-string v3.0.0 and pg v9.0.0), these modes will adopt standard libpq semantics, which have weaker security guarantees.      

To prepare for this change:
- If you want the current behavior, explicitly use 'sslmode=verify-full'
- If you want libpq compatibility now, use 'uselibpqcompat=true&sslmode=require'

See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode definitions.

This warning was introduced by this commit:

214function deprecatedSslModeWarning(sslmode) {
215 if (!deprecatedSslModeWarning.warned) {
216 deprecatedSslModeWarning.warned = true
217 emitWarning(`SECURITY WARNING: The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full'.
218In the next major version (pg-connection-string v3.0.0 and pg v9.0.0), these modes will adopt standard libpq semantics, which have weaker security guarantees.
219
220To prepare for this change:
221- If you want the current behavior, explicitly use 'sslmode=verify-full'
222- If you want libpq compatibility now, use 'uselibpqcompat=true&sslmode=${sslmode}'
223
224See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode definitions.`)
225 }
226}
227

and later released in

But the actual issue is the error that follows:

[14:48:23] ERROR: Error: cannot connect to Postgres. Details: self-signed certificate in certificate chain
    err: {
      "type": "Error",
      "message": "self-signed certificate in certificate chain",
      "stack":
          Error: self-signed certificate in certificate chain
              at C:\Users\somefolder\node_modules\pg-pool\index.js:45:11
              at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
              at async h (C:\Users\somefolder\.next\server\chunks\2624.js:308:5610)
              at async Object.i [as connect] (C:\Users\somefolder\.next\server\chunks\2624.js:308:5852)
              at async bG.init (C:\Users\somefolder\.next\server\chunks\2624.js:292:8722)
              at async bJ (C:\Users\somefolder\.next\server\chunks\2624.js:292:11911)
              at async r (C:\Users\somefolder\.next\server\chunks\9358.js:1:31438)
              at async generateRouteStaticParams (C:\Users\somefolder\node_modules\next\dist\build\static-paths\app.js:504:28)
              at async buildAppStaticPaths (C:\Users\somefolder\node_modules\next\dist\build\static-paths\app.js:579:25)
              at async C:\Users\somefolder\node_modules\next\dist\build\utils.js:685:79
      "code": "SELF_SIGNED_CERT_IN_CHAIN"
    }

Different meanings of sslmode=require#

And this has been known for 6 years:

After #2345 was merged, sslmode=require in connection string breaks with self signed certificate. SSL config is overriden to {}, leading to error "self signed certificate in certificate chain".

According to the table in #2281, correct SSL config is {rejectUnauthorized: false} in this case.

It is the result of different definitions and implementations of how “require” should behave.

libpq defines that “require” means “I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want.”

But the implementation in “node-postgres” was different: require behaved more like verify-full. Therefore, it checks the certificate chain and without further adjustments, it fails for OVHcloud and others, as many of them use self-signed certificates. This behavior will change in the near future.

This PR will align node-postgres to libpq when it gets released:

brianc/node-postgres #2709
pmalouin created on Feb 21, 2022 merged on Apr 20, 2025

Summary

We have found that the handling of the sslmode connection string parameter is inconsistent with other PG libraries and with the reference libpq documentation. This PR proposes some changes to sslmode behavior that are more aligned with libpq.

Detailed sslmode behavior

Here is the list of all sslmode values and their expected behavior with this PR:

sslmode=verify-full

Require an SSL connection and verify the CA and server identity.
No changes in this PR.

sslmode=verify-ca (changed)

Require an SSL connection and verify the CA, but not the server identity. This is achieved by setting ssl.checkServerIdentity to a no-op function (see docs). Previously, this mode behaved like verify-full but that was not consistent with the libpq implementation. SECURITY WARNING: Using sslmode=verify-ca requires specifying a CA with sslrootcert. If a public CA is used, verify-ca allows connections to a server that somebody else may have registered with the CA, making you vulnerable to Man-in-the-Middle attacks.

sslmode=require (changed)

If a root CA is provided via the sslrootcert parameter of the connection string, it behaves like verify-ca. Otherwise, require an SSL connection but do not verify CA and server identity (ssl.rejectUnauthorized is set to false).
Previously, this mode behaved like verify-full but that was not consistent with the libpq implementation.

sslmode=no-verify

Require an SSL connection but do not verify CA and server identity (ssl.rejectUnauthorized is set to false).
No changes in this PR. Note: this mode is not documented in libpq and does not appear to be broadly supported in other libraries, but has been kept as-is for the sake of backwards-compatibility. One option could be to mark it as deprecated since sslmode=require could be an alternative, but doing so might have little value for this project.

sslmode=prefer (changed)

Require an SSL connection but do not verify CA and server identity (ssl.rejectUnauthorized is set to false). Previously, this mode behaved like verify-full but that was not consistent with the libpq implementation.
In reality, this mode should be even less strict and support a fallback logic from SSL to non-SSL connection if SSL is not accepted by the server. Implementing a fallback logic seems to be more complex to solve and I did not dare touch this, but this could eventually be addressed if users of this library deem this mode valuable.

sslmode=allow

Not supported by this library.
No changes in this PR. For this mode also, there could be an opportunity to implement a fallback logic from non-SSL to SSL, but I did not dare touch this and I don't have data that suggests that this might be valuable for this project.

sslmode=disable

Only try a non-SSL connection.
No changes in this PR

An important note is that this PR potentially introduces semver breaking changes, in particular because it relaxes the security constraints of some sslmode values:

  • sslmode=prefer is less strict, users should switch to sslmode=verify-full to keep parity.
  • sslmode=require is less strict, users should switch to sslmode=verify-full to keep parity.
  • sslmode=verify-ca is less strict, users should switch to sslmode=verify-full to keep parity.

Prior discussions about sslmode

I believe this PR addresses concerns raised in these two GH issues in the past:
#2281
#2009

In particular, there has been one case where the sslmode=verify-ca is currently too strict when connecting through AWS RDS Proxy, but the work-around of using sslmode=no-verify would disable CA verification completely.

Other languages/libraries and their support for sslmode

Just as a reference, these two libraries are also trying to be more or less consistent with libpq:


Thanks for considering this change and please let me know how I can polish this further for acceptance 🙏

How to deal with this now?#

A: quick option: disable CA validation via URI parameter#

If you use a node-postgres-based stack, you can add “&uselibpqcompat=true” to the URL and activate the libpq definition of “require”. So overall this becomes “?sslmode=require&uselibpqcompat=true”.

B: use “sslrootcert” parameter#

Both libpq and node-postgres support the “sslrootcert” parameter, so by using “?sslmode=verify-full&sslrootcert=ca.pem” you can enable full server verification. The caveat here is that, at least in the payload/node-postgres environment, this reloads the certificate every time the connection string is parsed. Personally, I think this is not a good solution, because repeatedly loading the same file is a waste of resources.

C: TLS/SSL settings by code#

I prefer using explicit configuration in my source code to define which CA should be trusted for which connection string, and how the validation should happen. It also prevents repeatedly loading the same file again and again.

    // if the config has a connectionString defined, parse IT into the config we use
    // this will override other default values with what is stored in connectionString
    if (config.connectionString) {
      config = Object.assign({}, config, parse(config.connectionString))
    }

So I removed them from the URI and set everything explicit in code:

db: postgresAdapter({
    schemaName: 'payload',
    pool: {
      connectionString: process.env.DATABASE_URL || '',
      ssl: {
        ca: fs.readFileSync(path.resolve(dirname, '../sekskant-ca.pem'), 'utf8'),
        rejectUnauthorized: true,
      },
    },
  }),

How is the CA organized at OVHcloud?#

Without understanding the signing process that is used, it is hard to trust it.

I could not find any detailed documentation explaining how OVHcloud creates certificates for database instances and what the CA architecture looks like. The only official statement I found is:

OVHcloud generates an SSL/TLS certificate for each DB instance. Once you establish an encrypted connection between your application and your database instance, your data flows will be encrypted. https://help.ovhcloud.com/csm/es-public-cloud-databases-faq?id=kb_article_view&sysparm_article=KB0048919

I did some experiments, and because of that I have good reason to believe that:

  • There is one private shared CA, at least for PostgreSQL, used per cloud project.
  • The first PostgreSQL instance creates the CA.

Here is the evidence:

The downloadable CA has the issuer ”… Project CA”. It was also the same certificate for two instances in the same cloud project, and a different certificate for a PostgreSQL instance I created in another cloud project. The validity period also matches the creation date of the first database. This structure makes sense, because instead of needing one CA certificate per server, you only need one per cloud project.

This is how you can inspect the downloaded certificate:

openssl x509 -in cert.ca -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:ac:f4:8d:6c:a1:ae:66:b6:72:9b:24:b0:b3:7e:9b:23:7a:9a:58
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: CN=1633a89e-39db-4e5e-b175-21ed1d815617 Project CA
        Validity
            Not Before: Apr  8 22:27:00 2026 GMT
            Not After : Apr  5 22:27:00 2036 GMT
        Subject: CN=1633a89e-39db-4e5e-b175-21ed1d815617 Project CA

If you want to inspect the TLS handshake with the server live, you can use: openssl s_client -starttls postgres -connect servername.database.cloud.ovh.net:20184 -showcerts

If you want to provide the downloaded CA certificate, use the “-CAfile .<filename>” parameter.

Wildcard in the TLS certificate#

In addition to the real database hostnames, I noticed the wildcard DNS:*.database.cloud.ovh.net in the server certificate. This means the certificate is not bound to one specific hostname, but is valid for all database hostnames at OVHcloud. When the CA would be a shared one for all OVHcloud customers, that could be an issue. But in this case, the setup appears acceptable, even though it is still less strict than a certificate issued only for the exact hostname.

Certificate handling of other providers#

All providers have more or less different approaches to CA handling.

About the Author Marco Borm

Marco Borm

Founder of the software company sekskant based in Hoppegarten near Berlin. 26 years of professional experience as a team lead, developer, and bridge between customers, product management and software engineering.

On social media / Add a comment

More Articles

  • Payload + Turbopack + Windows: How to fix "Can't find stylesheet to import."
    DE | EN
    Apr 15, 2026
    Payload + Turbopack + Windows: How to fix "Can't find stylesheet to import."

    If you also have trouble running turbopack on windows and get "Can't find stylesheet to import." errors, this might help you.

  • Smartphone Training for Seniors on March 17, 2026 in Hönow/Hoppegarten
    DE | EN
    Mar 29, 2026
    Smartphone Training for Seniors on March 17, 2026 in Hönow/Hoppegarten

    This year, I am once again offering smartphone training sessions for seniors on several dates. One of them took place on March 17.

  • OVHcloud + VPS: Still no Ubuntu 25.10, only Ubuntu 25.04
    DE | EN
    Mar 27, 2026
    OVHcloud + VPS: Still no Ubuntu 25.10, only Ubuntu 25.04

    When setting up a VPS, it is worth checking which images the provider actually offers. At OVHcloud, the newest available Ubuntu version is still 25.04, even though it has already been out of support for a while. I asked them about it.