Skip to main content
    Guides

    SSL/TLS Certificates for Databases: PostgreSQL and MySQL

    How to choose and install a TLS certificate for PostgreSQL or MySQL: public CA vs private CA, verify-ca vs verify-full, and renewal without a restart.

    MS
    My-SSL Team
    ·
    14 min read
    ·
    Published September 8, 2026
    ·
    Last updated September 8, 2026

    The short answer

    A database needs the same object a web server needs: a TLS server certificate with the serverAuth extended key usage and a subject alternative name matching the hostname in the connection string. Who signs it comes down to one question — whether that hostname resolves in public DNS, because a publicly trusted CA is not permitted to issue for a name that does not. The decision that actually costs you something is renewal, not issuance. As of 15 March 2026 a publicly trusted certificate lasts at most 200 days, falling to 100 in March 2027 and 47 in March 2029, so any database whose certificate swap needs a restart is booking maintenance windows several times a year. Both engines have avoided that for years: SELECT pg_reload_conf() on PostgreSQL 10 and later, ALTER INSTANCE RELOAD TLS on MySQL 8.0.16 and later.

    Settle the signer before you generate anything, because it decides which recurring job you inherit. If clients reach the database at a public DNS name — a managed instance, a replica a partner connects to, an analytics endpoint outside your own network — a publicly trusted certificate removes the trust-distribution problem entirely, and the certificate types and validation levels we issue set out what each level asks you to prove. If the hostname exists only inside your network, no public CA may issue for it and a private CA is the route.

    Decision diagram showing which kind of certificate authority should sign a database TLS certificate, based on whether the hostname resolves in public DNSA single question sits at the top of the diagram: does the hostname in the connection string resolve in public DNS. Three branches lead away from it. The first branch, for a publicly resolvable name such as db dot example dot com, leads to a publicly trusted certificate authority; clients verify it against the operating system trust store, so there is nothing to install on them, and the ongoing job is renewal every two hundred days or less. The second branch, for an internal-only name such as pg dash zero one dot internal, leads to a private certificate authority; no public authority is permitted to issue for that name, and the ongoing job is distributing the private root to every client library and keystore. The third branch, for a local development machine, leads to a self-signed certificate, which every client must be told to trust individually and which cannot pass hostname verification unless the name is present in the certificate. A bar across the bottom notes that the certificate itself is identical in all three cases; only the signer and the resulting distribution work differ.One question decides who signs itDoes the hostname in the connection stringresolve in public DNS?YES — db.example.comPublicly trusted CAClients verify against theOS trust store they alreadyhave. Nothing to install.Ongoing job:renew every 200 daysor less, without arestart.NO — pg-01.internalPrivate CANo public CA may issuefor this name, and DCVcould not succeed anyway.Ongoing job:get the private root intoevery driver, JVM andcontainer image.LAPTOP ONLY — localhostSelf-signedFine for a dev databasenobody else connects to.Trusted per client, by hand.Ongoing job:none, until someonecopies the setup intostaging.The certificate is the same object in all three cases: serverAuth EKU, a SAN matching the hostname.Only the signer differs — and with it, who has to be told to trust it.
    The branch you land on is not really a certificate decision. It is a decision about which recurring job you would rather own: renewals, or trust distribution.

    Which SSL certificate does a database actually need?

    An ordinary TLS server certificate, with nothing special about it. It needs the serverAuth extended key usage, a subject alternative name matching the hostname clients dial, and a chain the clients can build to a root they trust. No certificate authority sells a “database certificate” because there is no such field in X.509 to sell. Validation level is a business question, not a technical one: DV encrypts and identifies the name exactly as well as OV does.

    What changes is who is looking. A web certificate is judged by browsers, which ship a trust store, update it silently, and complain loudly in a way a human notices. A database certificate is judged by client libraries — libpq, JDBC, psycopg, Npgsql, the MySQL connectors — each with its own trust configuration, its own defaults, and no interface to shout at anybody. The failure mode is not a warning page. It is a connection that either fails at three in the morning or silently skips verification for a year.

    One structural point is worth settling early because it changes what you buy. A TLS endpoint is a hostname, and each hostname that terminates its own connection needs a certificate valid for it. A primary at db.example.com, a read replica at replica.example.com and a PgBouncer in front at pool.example.com are three endpoints. Putting the three names on one multi-domain certificate gives you one renewal instead of three, which matters more every year as the windows shrink.

    When is a publicly trusted certificate the right choice?

    When the hostname is publicly resolvable and the clients are not all yours to configure. A publicly trusted certificate is verified against the trust store the operating system already ships, so a contractor's laptop, a customer's ETL job and a managed analytics service all validate it without you touching them. That is the entire benefit, and on an estate with more than a handful of client types it is a large one.

    The constraint is the name. Publicly trusted CAs have been barred from issuing for internal names and reserved IP ranges since 2015, so pg-01.internal and 10.0.4.19 are out, and domain control validation could not succeed for them in any case. What people miss is that this rules out the name, not the address: a database that only listens on a private IP can still hold a certificate for db.example.com, because validation happens in DNS and never involves connecting to the host. Split-horizon DNS with a public record for validation and a private answer for clients is a normal, supported arrangement, and our guide to certificates for internal server names works through the mechanics.

    A private CA earns its place when the estate is closed and uniform: every client is yours, every trust store is centrally managed, and the hostnames are internal by design. It also handles cases a public CA cannot touch, which now includes client certificates. The cost is real and recurring — root distribution into every JVM keystore, container image and language runtime, plus revocation and root rollover as ongoing work — and it lands on the same team either way.

    Certificate Transparency is the third input to this decision and the one that surprises people. Every publicly trusted certificate is logged, publicly and permanently, so issuing for payments-db-prod.example.com publishes that hostname to anyone reading the logs. That is not a reason to avoid public certificates. It is a reason to pick database hostnames that you are content to have read, which costs nothing if you decide it before the first order rather than after.

    verify-ca vs verify-full: the weaker setting is the dangerous one

    Use verify-full. It is the only PostgreSQL mode that checks the hostname you dialled against the certificate you were served; verify-ca stops after the chain check. With a private CA that gap is narrow, because only your own CA's certificates pass. With a publicly trusted CA the gap is the whole internet: any certificate that CA has issued to anyone, for any name, satisfies verify-ca. The PostgreSQL manual says so directly.

    Diagram contrasting verify-ca and verify-full, showing that only verify-full rejects a valid certificate issued for a different hostnameTwo panels side by side follow the same machine-in-the-middle attempt. In both, an attacker intercepts the connection and presents a genuine, unexpired certificate that a public certificate authority issued to them for a name they really control, attacker dot example. The left panel, labelled verify-ca, runs two checks: the chain is valid, so the check passes, and the hostname is never compared, so the connection is accepted and the attacker reads the traffic. The right panel, labelled verify-full, runs the same chain check and then compares the hostname the client dialled, db dot example dot com, against the subject alternative names in the certificate; they do not match, so the connection is refused. A bar across the bottom states the consequence: under verify-ca with a publicly trusted certificate authority, every certificate that authority has ever issued to anyone is accepted as your database.Same attacker. Same real certificate. Two outcomes.Client dials db.example.com · attacker presents a valid cert for attacker.examplesslmode=verify-caMySQL equivalent: --ssl-mode=VERIFY_CA1 · Chain signed by a trusted root?Yes — it is a genuine public certificate2 · Hostname matches a SAN?Never checkedCONNECTION ACCEPTEDQueries and credentials go to the attackersslmode=verify-fullMySQL equivalent: --ssl-mode=VERIFY_IDENTITY1 · Chain signed by a trusted root?Yes — same certificate as the left panel2 · Hostname matches a SAN?db.example.com ≠ attacker.exampleCONNECTION REFUSEDserver certificate for "attacker.example"Under verify-ca with a public CA, every certificate that CA has ever issued to anyoneis accepted as your database.
    This is why the PostgreSQL manual tells you to use verify-full whenever the root is a public CA: verify-ca is a chain check with no identity in it.

    MySQL draws the same line one step further along its ladder. VERIFY_CA validates the chain, and VERIFY_IDENTITY adds the hostname check. The trap in MySQL is different and more common: the server generates a self-signed certificate at first startup if none is configured, and that certificate carries no server name, so VERIFY_IDENTITY cannot succeed against it. Teams hit the failure, drop to VERIFY_CA to make it go away, and ship that.

    PostgreSQLMySQLWhat it actually does
    disableDISABLEDPlaintext on the wire.
    prefer (libpq default)PREFERRED (default)Encrypts if the server offers it, silently falls back to plaintext if it does not.
    requireREQUIREDEncryption is mandatory; the certificate is not examined at all.
    verify-caVERIFY_CAChain validated against a configured root. Hostname ignored.
    verify-fullVERIFY_IDENTITYChain validated and the hostname matched against the SANs. The only row that resists an active attacker.

    Both defaults deserve a moment of attention, because they are the reason so many production connections are unverified. libpq defaults to prefer and MySQL to PREFERRED, and neither will tell you it downgraded. A connection string with no explicit mode in it is a connection you have made no promises about.

    How to set up TLS on PostgreSQL

    Three settings in postgresql.conf and one file permission. Set ssl = on, point ssl_cert_file at the certificate concatenated with its intermediates and ssl_key_file at the private key, then reload. The key must not be readable by group or world: PostgreSQL refuses to start otherwise, and that single check accounts for most first-attempt failures.

    # postgresql.conf
    ssl = on
    ssl_cert_file = '/etc/postgresql/tls/server.crt'   # leaf + intermediates
    ssl_key_file  = '/etc/postgresql/tls/server.key'
    ssl_ca_file   = '/etc/postgresql/tls/root.crt'     # only for client certs

    The permission rule has a second form that matters far more than its obscurity suggests. The key may be 0600 owned by the database user, or 0640 owned by root with the database user in the owning group. That second form is what makes automated renewal workable, because ACME clients write as root and you would otherwise be chowning private keys in a deploy hook. Set it up once and every future renewal lands correctly on its own.

    ssl_ca_file is not part of serving a certificate, which is a routine source of confusion. It is the root PostgreSQL uses to verify client certificates, so leave it unset unless you are doing mutual TLS. Requiring encryption is a separate job again, done in pg_hba.conf: change host lines to hostssl, or add a hostnossl ... reject line, otherwise a client that skips TLS is still welcome.

    On the client side, the connection string carries the promise. Ask for sslmode=verify-full and give it a root to verify against with sslrootcert. PostgreSQL 16 added a much better answer for publicly trusted certificates: sslrootcert=system uses the operating system's own trust store, which means no CA file to ship, no path to keep in sync, and nothing to update when a root changes. Because a system store without a hostname check would be actively unsafe, libpq raises the default to verify-full when you use it.

    # Publicly trusted certificate, PostgreSQL 16+ client
    psql "host=db.example.com dbname=app user=app \
          sslmode=verify-full sslrootcert=system"
    
    # Private CA, or a client older than 16
    psql "host=db.example.com dbname=app user=app \
          sslmode=verify-full sslrootcert=/etc/ssl/certs/internal-root.crt"

    How to set up TLS on MySQL

    Point three variables at your files in the [mysqld] section and restart once. MySQL 8.0 will already be serving TLS when you get there, using a self-signed certificate it generated at first startup, so the job is usually replacing something rather than enabling it. That auto-generated certificate is also why VERIFY_IDENTITY fails before you start: it carries no server name.

    # my.cnf
    [mysqld]
    ssl_cert = /etc/mysql/tls/server.crt   # leaf + intermediates
    ssl_key  = /etc/mysql/tls/server.key
    ssl_ca   = /etc/mysql/tls/root.crt     # only for client certs
    require_secure_transport = ON

    require_secure_transport = ON is the setting that turns encryption from available into mandatory; without it, clients that ask for plaintext get plaintext, and the server-side certificate work protects nobody in particular. Per-account rules give you a finer grip when a full switch would break a legacy job: ALTER USER ... REQUIRE SSL for one account, REQUIRE X509 when that account must also present a client certificate.

    Clients then need to be told to check what they are given, and MySQL is quieter about this than PostgreSQL because its default is PREFERRED. Ask for the top rung explicitly:

    mysql --host=db.example.com --user=app \
          --ssl-mode=VERIFY_IDENTITY \
          --ssl-ca=/etc/ssl/certs/ca-certificates.crt
    
    # Confirm what you actually negotiated
    mysql> SHOW STATUS LIKE 'Ssl_cipher';
    mysql> SHOW STATUS LIKE 'Ssl_server_not_after';

    MySQL has no equivalent of sslrootcert=system, so even a publicly trusted certificate needs --ssl-ca pointed at a CA bundle. On most Linux hosts the operating system bundle at /etc/ssl/certs/ca-certificates.crt is the right file, and using it rather than a copied-out root is what keeps the setup working through a root rollover you never hear about. Ssl_server_not_after is worth knowing on its own: it is the expiry of the certificate the server is serving right now, which is exactly what you check after a reload.

    How to renew a database certificate without restarting

    Write the new files over the old paths and issue one reload statement. PostgreSQL has re-read its TLS files on SIGHUP since version 10, so SELECT pg_reload_conf() is enough. MySQL 8.0.16 added ALTER INSTANCE RELOAD TLS, which rebuilds the TLS context in place and needs the CONNECTION_ADMIN privilege. MariaDB 10.4 added FLUSH SSL for the same purpose.

    Diagram of a zero-restart certificate rotation on a database server, with the reload command for PostgreSQL, MySQL and MariaDBA three-step sequence runs left to right. Step one: the renewal process writes the new certificate and key over the existing file paths, which must not change, because the configuration variables holding those paths cannot be altered without a restart. Step two: a single reload command rebuilds the server's TLS context in place. Three rows give the command per engine: PostgreSQL version 10 and later uses SELECT pg_reload_conf, or a SIGHUP; MySQL version 8.0.16 and later uses ALTER INSTANCE RELOAD TLS, which requires the CONNECTION ADMIN privilege; MariaDB version 10.4 and later uses FLUSH SSL. Step three: new connections negotiate with the new certificate, while sessions already open keep the context they negotiated and are not interrupted. A note underneath warns that if the new files cannot be loaded, MySQL rolls the reload back and keeps serving the old context, so a reload that reports success is not on its own proof that the new certificate is in use.Rotating a database certificate without a restart1 · WRITE IN PLACEOverwrite the existingcert and key files.Paths must not change.2 · RELOAD THE CONTEXTOne statement rebuildsthe TLS context fromthe files on disk.3 · NEW CONNECTIONSGet the new certificate.Open sessions keep theold one, uninterrupted.THE RELOAD COMMAND, BY ENGINEPostgreSQL 10+SELECT pg_reload_conf();or a SIGHUP to the postmasterMySQL 8.0.16+ALTER INSTANCE RELOAD TLS;needs CONNECTION_ADMINMariaDB 10.4+FLUSH SSL;also reloads Galera when WSREP is onMySQL rolls the reload back and keeps the old context if the new files will not load.Check the served certificate afterwards, not just the command's exit status.
    Every one of these commands exists because someone had to restart a production database to install a certificate. At 47-day lifetimes that is eight planned restarts a year you no longer have to schedule.

    Two properties of these commands decide how you build the automation around them. The paths cannot change, because the variables holding them are not dynamic — the reload re-reads the files at the configured locations, so a renewal that writes to a new dated directory has to be followed by a symlink swap or a copy into place. And the new context applies to new connections only. Sessions already open finish on the certificate they negotiated, which is what makes the operation genuinely non-disruptive, and also why a pooled application may take a while to visibly pick up the new certificate.

    MySQL rolls the reload back if the new files will not load, keeps serving the previous context, and returns an error. That is good behaviour and a bad trap for a script, because a half-written file from a renewal that was still running leaves you with a healthy database and a certificate that is not the one you think you deployed. Verify after the reload rather than trusting the exit status: SHOW STATUS LIKE 'Ssl_server_not_after' on MySQL, or an openssl s_client -starttls postgres probe against PostgreSQL.

    A working ACME deploy hook for PostgreSQL is four lines, and the ownership line is the one people leave out:

    #!/bin/sh
    # certbot --deploy-hook, or the equivalent in acme.sh / lego
    install -o root -g postgres -m 0640 "$LIVE/privkey.pem" /etc/postgresql/tls/server.key
    install -o root -g postgres -m 0644 "$LIVE/fullchain.pem" /etc/postgresql/tls/server.crt
    psql -U postgres -c 'SELECT pg_reload_conf();'

    DNS-01 is nearly always the right challenge type here, because a database host has no reason to be serving HTTP on port 80 and every reason not to. Which ACME challenge your setup can actually use covers the trade-offs, including the delegation pattern that keeps DNS API credentials off the database host.

    What 200-day certificates change for a database estate

    They convert a background task into a scheduled one, and then into an automated one. A publicly trusted certificate issued today may last 200 days at most; from 15 March 2027 that becomes 100 days, and from 15 March 2029, 47 days. Domain validation reuse falls on the same schedule and lands much lower, at 10 days, so by 2029 each name is re-proved roughly 35 times a year rather than renewed 8 times.

    Timeline of shrinking publicly trusted certificate lifetimes and domain validation reuse periods, with the resulting number of renewals per yearA timeline runs left to right through three dated steps set by CA/Browser Forum ballot SC-081v3. From the fifteenth of March 2026, the maximum certificate lifetime is two hundred days and domain validation may be reused for two hundred days, which is about two renewals per year for each database endpoint. From the fifteenth of March 2027, both fall to one hundred days, about four renewals per year. From the fifteenth of March 2029, the certificate lifetime falls to forty-seven days while validation reuse falls much further, to ten days, which is roughly eight renewals per year and about thirty-five separate proofs of domain control. A note underneath observes that the renewal count is the manageable part; the validation reuse column is the one that removes any possibility of doing this by hand.What a database endpoint has to absorb by 2029CA/Browser Forum ballot SC-081v3 · publicly trusted TLS certificates15 Mar 202615 Mar 202715 Mar 2029200 daysmax certificate lifetimeValidation reuse: 200 days~2 renewals / yearin force today100 daysmax certificate lifetimeValidation reuse: 100 days~4 renewals / yeara maintenance window each quarter47 daysmax certificate lifetimeValidation reuse: 10 days~8 renewals / year~35 proofs of domain controlEight renewals a year is a calendar problem. Thirty-five domain validations a year is not.The reuse column is what makes automation the only workable answer.
    Renewal counts are approximate: they assume you renew at the end of each window rather than early, which nobody sensible does.

    Databases absorb this worse than web servers for reasons that have nothing to do with TLS. Change control is heavier, the maintenance window is a negotiation, and the fleet is long-lived: an instance provisioned in 2021 with a three-year certificate has never had a renewal, so nobody on the current team has done one. The first renewal under the new regime is therefore also the first test of a procedure that may not exist, and it arrives on a deadline.

    The practical move is to decouple the two halves now, while there is slack. Automate acquisition — ACME with DNS-01, or a scripted order and install — and automate deployment with the reload commands above, then run the whole loop once on a replica with a deliberately short certificate to prove it works end to end. A rehearsal on a replica costs an afternoon. Our broader guide to preparing for shorter certificate lifetimes covers the same problem across the rest of the estate.

    Monitoring needs one adjustment that is easy to miss. Alerting on the certificate file on disk tells you what the renewal process wrote, not what the server is serving, and after a failed reload those are different things for months. Probe the live port and read the certificate off the wire. On PostgreSQL that means an openssl s_client -starttls postgres check; on MySQL, Ssl_server_not_after straight from the running server.

    The part that breaks: trust on every client

    Server configuration is an afternoon; client trust is the project. A database is dialled by application pods, batch jobs, BI tools, admin laptops and whatever a contractor installed in 2023, and each one decides for itself which roots to trust and where to find them. A publicly trusted certificate collapses that work, because every one of those clients already ships a trust store containing the root.

    A private CA does not get that for free, and the list of places its root has to be installed is longer than the plan assumes: thecacerts keystore of every JVM, container images that bake their own CA bundle, Python and Node.js runtimes with their own trust handling, Go binaries built with a pinned bundle, and every developer machine. None of these is difficult. There are simply many of them, they are discovered one at a time, and each is discovered by something breaking.

    Mutual TLS is where the two routes stopped being interchangeable during 2026. Publicly trusted TLS certificates no longer carry the clientAuth extended key usage, so PostgreSQL clientcert=verify-full and MySQL REQUIRE X509 now need client certificates from a private CA regardless of what signs the server side. The removal of clientAuth from public TLS certificates sets out the dates and what replaces it. A public certificate on the server with a private CA for clients is a perfectly reasonable arrangement, and increasingly the only one available.

    What each failure message is telling you

    Database TLS errors are unusually literal, and the same handful account for nearly all of them. The table below maps each to its actual cause, which is rarely the certificate itself.

    What you seeWhat it means
    private key file has group or world accessThe key is not 0600 owned by the server user or 0640 owned by root. A renewal wrote it with default permissions.
    server certificate for "X" does not match host nameverify-full is working correctly. The connection string uses a name, or an IP, that is not in the SANs.
    unable to get local issuer certificateThe chain is incomplete. The intermediates were not concatenated into the certificate file — use fullchain, not cert.
    self-signed certificate in certificate chainUsually MySQL still serving its auto-generated startup certificate, because the ssl_cert path is wrong and the server fell back silently.
    Certificate on disk is new, clients still see the old oneNo reload ran, or MySQL rolled one back. Read the expiry off the wire rather than off the filesystem.
    Everything works, and nothing is verifiedThe connection string carries no explicit mode, so it defaults to prefer or PREFERRED. The quietest failure of the set.

    The last row is the one worth auditing deliberately, because nothing will ever surface it for you. Enforce the floor at the server with hostssl or require_secure_transport, so a client that forgot to ask for verification is refused rather than quietly accommodated.

    FAQ

    Frequently Asked Questions

    Get instant answers to common questions about SSL certificates and our services.

    Still Have Questions?

    Our SSL experts are available 24/7 to help with any questions about certificates, installation, or technical issues.

    For database endpoints with a public name

    If your database answers to a hostname that resolves in public DNS, a publicly trusted certificate takes the trust-distribution work off your plate for every client that connects. My-SSL issues through Certum, a publicly trusted certificate authority whose roots ship in the major browser and operating system trust stores, which is the same store sslrootcert=system and ca-certificates.crt read from. The certificate range and validation levels cover single hostnames, multi-domain certificates for a primary and its replicas, and wildcards where the endpoint names change more often than you would like.