
OVHcloud + Managed PostgreSQL: Korrekte Zertifikatsvalidierung mit dem node-"pg"-Modul und Payload
Einleitung#
Viele Managed-Database-Dienste, wie der von OVHcloud, bieten TLS-gesicherte Verbindungen zu deiner Datenbank an. Aber bedeutet das in deinem Kontext wirklich, dass du sicher bist, oder bleibst du trotzdem ein gutes Ziel für Man-in-the-Middle-Angriffe? Dieser Artikel teilt einige Erkenntnisse aus meiner eigenen Erfahrung.
Das Problem#
sslmode=require: Warnungen und Fehler#
Wenn du zum Beispiel OVHcloud verwendest, kannst du den Connection String aus der UI einfach kopieren und in deine Anwendung übernehmen.
Der von OVHcloud enthält, wie viele andere auch, den Parameter sslmode=require, aber das reicht nicht aus, um dich gegen einen Man-in-the-Middle-Angriff zu schützen. Er schützt dich nur gegen passives Mitschneiden in deinem nicht vertrauenswürdigen Netzwerk.
Das ist wichtig, aber damit ist die Arbeit noch nicht erledigt.
Wenn du diesen String in einem Stack verwendest, der von node-postgres/pg ↗ abhängt, läufst du wahrscheinlich in diese Warnung:
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.
Diese Warnung wurde durch diesen Commit eingeführt:
| 214 | function 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'. | ||
| 218 | 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. | ||
| 219 | |||
| 220 | To 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 | |||
| 224 | See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode definitions.`) | ||
| 225 | } | ||
| 226 | } | ||
| 227 |
und später in diesem Release veröffentlicht:
Das eigentliche Problem ist aber der Fehler, der danach folgt:
[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"
}
Unterschiedliche Bedeutungen von sslmode=require#
Und das ist seit 6 Jahren bekannt:
Es ist das Ergebnis unterschiedlicher Definitionen und Implementierungen davon, wie require sich verhalten soll.
libpq ↗ definiert require als „Ich möchte, dass meine Daten verschlüsselt sind, und akzeptiere den Overhead. Ich vertraue darauf, dass das Netzwerk sicherstellt, dass ich mich immer mit dem Server verbinde, den ich haben will.“
Die Implementierung in node-postgres war jedoch anders: require verhielt sich eher wie verify-full.
Dadurch wird die Zertifikatskette geprüft, und ohne weitere Anpassungen schlägt das bei OVHcloud und anderen Anbietern typischerweise fehl, weil viele von ihnen selbst signierte Zertifikate verwenden.
Dieses Verhalten wird sich in naher Zukunft ändern.
Dieser PR wird node-postgres beim Release an libpq angleichen:
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=preferis less strict, users should switch tosslmode=verify-fullto keep parity.sslmode=requireis less strict, users should switch tosslmode=verify-fullto keep parity.sslmode=verify-cais less strict, users should switch tosslmode=verify-fullto 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:
- Golang - pq
- Supports
disable,require,verify-caandverify-fullonly.
- Supports
- PostgreSQL JDBC Driver
- Supports all 6 modes.
Thanks for considering this change and please let me know how I can polish this further for acceptance 🙏
Was nun also tun?#
A: Schnelle Variante: CA-Validierung per URI-Parameter deaktivieren#
Wenn du einen node-postgres-basierten Stack verwendest, kannst du &uselibpqcompat=true an die URL anhängen und damit die libpq-Definition von require aktivieren. Insgesamt wird daraus also ?sslmode=require&uselibpqcompat=true.
B: Den sslrootcert-Parameter verwenden#
Sowohl libpq als auch node-postgres unterstützen den Parameter sslrootcert. Mit ?sslmode=verify-full&sslrootcert=ca.pem kannst du also eine vollständige Server-Validierung aktivieren.
Der Haken ist, dass in der payload/node-postgres-Umgebung das Zertifikat jedes Mal neu geladen wird, wenn der Connection String geparst wird.
Ich halte das persönlich nicht für eine gute Lösung, weil das wiederholte Laden derselben Datei Ressourcen verschwendet.
C: TLS/SSL-Einstellungen direkt im Code setzen#
Ich bevorzuge eine explizite Konfiguration im Code, um festzulegen, welcher CA für welchen Connection String vertraut werden soll und wie die Validierung erfolgen soll. Außerdem verhindert das, dass dieselbe Datei immer wieder neu geladen wird.
// 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))
} Also habe ich alle TLS/SSL Parameter aus der URI entfernt und setze die Einstellungen explizit so im Code:
db: postgresAdapter({
schemaName: 'payload',
pool: {
connectionString: process.env.DATABASE_URL || '',
ssl: {
ca: fs.readFileSync(path.resolve(dirname, '../sekskant-ca.pem'), 'utf8'),
rejectUnauthorized: true,
},
},
}),
Wie ist die CA bei OVHcloud organisiert?#
Ohne den verwendeten Signaturprozess zu verstehen, ist es schwer, dem Setup zu vertrauen.
Ich konnte keine detaillierte Dokumentation finden, die erklärt, wie OVHcloud Zertifikate für Datenbankinstanzen erstellt und wie die CA-Architektur aussieht. Die einzige offizielle Aussage, die ich gefunden habe, ist:
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 ↗
Ich habe ein paar Experimente gemacht und habe deshalb gute Gründe anzunehmen:
- Es gibt mindestens für PostgreSQL eine private gemeinsame CA pro Cloud-Projekt.
- Die erste PostgreSQL-Instanz erstellt diese CA.
Hier ist die Begründung:
Die herunterladbare CA trägt den Issuer .... Project CA. Außerdem war es für zwei Instanzen im selben Cloud-Projekt dasselbe Zertifikat und für eine PostgreSQL-Instanz in einem anderen Cloud-Projekt ein anderes Zertifikat.
Auch der Gültigkeitszeitraum passt zum Erstellungsdatum der ersten Datenbank.
Diese Struktur ergibt Sinn, weil man so nicht ein CA-Zertifikat pro Server benötigt, sondern nur eines pro Cloud-Projekt.
So kannst du das heruntergeladene Zertifikat prüfen:
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
Wenn du den TLS-Handshake live mit dem Server untersuchen möchtest, kannst du Folgendes verwenden: openssl s_client -starttls postgres -connect servername.database.cloud.ovh.net:20184 -showcerts
Wenn du das heruntergeladene CA-Zertifikat mitgeben möchtest, verwende den Parameter -CAfile .\<filename>.
Wildcard im TLS-Zertifikat#
Zusätzlich zu den eigentlichen Datenbank-Hostnamen ist mir das Wildcard-Pattern DNS:*.database.cloud.ovh.net im Serverzertifikat aufgefallen. Das bedeutet, dass das Zertifikat nicht an einen einzelnen Hostnamen gebunden ist, sondern für alle Datenbank-Hostnamen unter dieser Domain gültig ist.
Wenn die ausstellende CA für alle OVHcloud-Kunden gemeinsam wäre, wäre das problematischer.
In diesem Fall wirkt das Setup jedoch akzeptabel, auch wenn es weniger strikt ist als ein Zertifikat, das nur für den exakten Hostnamen ausgestellt wurde.
Zertifikatshandhabung bei anderen Anbietern#
Alle Anbieter haben mehr oder weniger unterschiedliche Ansätze für die CA-Verwaltung.
- AWS RDS verwendet Amazon-RDS-CA-Zertifikate, um die DB-Serverzertifikate zu signieren, und empfiehlt den Download des CA-Bundles zur Verifikation: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html ↗
- Azure Database for PostgreSQL dokumentiert derzeit Zertifikatsketten, die auf öffentlichen/vertrauenswürdigen Roots wie DigiCert Global Root G2 und Microsoft RSA Root CA 2017 basieren: https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/security-tls ↗
- Google Cloud SQL kann verschiedene CA-Hierarchien verwenden, darunter eine CA pro Instanz, eine gemeinsame CA oder eine kundenseitig verwaltete CA: https://cloud.google.com/sql/docs/postgres/manage-ssl-instance ↗
- Supabase empfiehlt den Download des eigenen CA-Zertifikats für
verify-full: https://supabase.com/docs/guides/platform/ssl-enforcement ↗

