Database transport encryption protects the connection between your application and the database. Without it, authentication credentials, queries and results travel as plain text, readable by anyone who can capture traffic on the path. This page documents the 5 checks in the CtrlOps Database Transport Encryption audit, in the order the audit runs them.
It is a distinct layer from encryption at rest (which protects stored data) and from authentication (which verifies who connects). Every threshold below is transcribed from the audit script itself, not from general advice.
Key takeaways
Five checks across three engines: two about whether TLS is required, two about whether the certificate is about to break, and one that combines both questions for MongoDB.
- All five are MEDIUM severity. None of them lets an attacker in on its own; each one decides whether an attacker already on the network path can read what passes.
- All five need root, because each opens an administrative connection to read a server variable or reads a certificate file that is deliberately not world-readable.
- The whole audit takes about 11 seconds when CtrlOps runs it over your existing SSH connection, against roughly 10 to 15 minutes by hand.
- Three checks are graded against the bind address. Whether TLS being off is a warning or a failure depends on whether the engine is reachable from off the host, so this audit reads the same values as the network isolation audit.
| # | Check | Engine | Severity | What it catches |
|---|---|---|---|---|
| 1 | MySQL enforcement | MySQL / MariaDB | MEDIUM | TLS available but not required |
| 2 | MySQL certificate | MySQL / MariaDB | MEDIUM | Certificate expiring within 30 days |
| 3 | PostgreSQL TLS | PostgreSQL | MEDIUM | ssl off while listening beyond localhost |
| 4 | PostgreSQL certificate | PostgreSQL | MEDIUM | Certificate expiring within 30 days |
| 5 | MongoDB TLS | MongoDB | MEDIUM | No requireTLS, weighted by bindIp |
tls-port yourself; nothing here will tell you whether you did.Check 1: Does MySQL require encrypted connections?
MySQL can support TLS without requiring it, and that is the default. When require_secure_transport is off, a client may connect encrypted or in plain text, and most client libraries choose plain text unless told otherwise. So the credentials go across the wire in the clear on a server that looks, from the configuration, as though TLS is set up.
The audit reads three variables and grades the combination: require_secure_transport, have_ssl (falling back to have_openssl on MariaDB builds), and skip_networking.
How to check manually
sudo mysql -N -e "SELECT @@require_secure_transport, @@skip_networking"
sudo mysql -e "SHOW VARIABLES LIKE 'have_ssl'"
sudo mysql -e "SHOW VARIABLES LIKE 'have_openssl'"| Result | When | What it means |
|---|---|---|
| PASS | require_secure_transport is on, or skip_networking is on | Plaintext connections are refused, or TCP is disabled entirely so network TLS is moot. |
| WARN | TLS is available but not required | Clients may connect unencrypted and most default to it. Set require_secure_transport to ON. |
| FAIL | TLS is not available at all | Every connection is plain text and there is nothing to enforce. Configure ssl_cert and ssl_key first. |
| SKIP | MySQL is not installed, or is not accessible | The two reasons are reported separately. |
The skip_networking pass is worth understanding. It disables TCP entirely and leaves only the Unix socket, which means there is no network path to encrypt. That is a legitimate and strong configuration when the application runs on the same host, and it passes here for the same reason it passes the network isolation audit.
How to fix it
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_key = /etc/mysql/ssl/server-key.pem
ssl_ca = /etc/mysql/ssl/ca-cert.pem
require_secure_transport = ON
sudo systemctl restart mysqlrequire_secure_transport = ON refuses every plaintext connection from the moment the server comes back, so an application whose driver has not been told to use TLS fails instantly. Change the connection settings first, deploy, confirm the application still works with TLS optional, and only then turn on enforcement.Check 2: Is the MySQL certificate about to expire?
TLS is only as available as the certificate behind it. An expired server certificate does not weaken the connection, it breaks it: any client that verifies certificates refuses to connect, and the application goes down.
The audit asks the running server for its ssl_cert path, resolves a relative path against the data directory, and runs openssl x509 -checkend 2592000, which is 30 days in seconds.
How to check manually
CERT=$(sudo mysql -N -e "SELECT @@ssl_cert")
sudo openssl x509 -checkend 2592000 -noout -in "$CERT" && echo "more than 30 days" || echo "under 30 days"
sudo openssl x509 -enddate -noout -in "$CERT"| Result | When | What it means |
|---|---|---|
| PASS | The certificate is valid for more than 30 more days | No action needed yet. |
| WARN | The certificate expires within 30 days | Rotate it before clients start failing. This is a scheduled outage if you ignore it. |
| SKIP | No certificate is configured, the configured file does not exist on disk, or MySQL is not accessible | All three reasons are reported separately. No certificate configured is check 1 to fix, not this one. |
A SKIP for "configured but not found" is the one to look at closely: it means ssl_cert names a path the server cannot read, so TLS is not working despite appearing configured.
How to fix it
# Generate a replacement
openssl req -new -x509 -days 365 -nodes \
-keyout /etc/mysql/ssl/server-key.pem \
-out /etc/mysql/ssl/server-cert.pem \
-subj "/CN=$(hostname)"
sudo chmod 600 /etc/mysql/ssl/server-key.pem
sudo chown mysql:mysql /etc/mysql/ssl/*
sudo systemctl restart mysqlCheck 3: Is PostgreSQL TLS switched on?
PostgreSQL defaults to ssl = off on many distributions, which means every connection, including the authentication exchange, is unencrypted.
What makes this check better than a plain on-or-off test is that it reads listen_addresses at the same time and grades the answer against it. TLS off on a cluster that only listens on localhost is a different situation from TLS off on one reachable across the network.
How to check manually
sudo -u postgres psql -X -tAc 'SHOW ssl'
sudo -u postgres psql -X -tAc 'SHOW listen_addresses'| Result | When | What it means |
|---|---|---|
| PASS | ssl is on | TLS is available. Pair it with hostssl rules to make it mandatory rather than optional. |
| WARN | ssl is off and the cluster listens on localhost only | Tolerable for now, since nothing crosses a network. Mandatory before you expose 5432. |
| FAIL | ssl is off while listening beyond localhost | Credentials and data cross the network in clear text. |
| SKIP | PostgreSQL is not installed, or is not accessible | The two reasons are reported separately. |
Note what a PASS here does and does not mean. ssl = on makes TLS possible. A host line in the authentication file still accepts an unencrypted connection from a client that does not ask for TLS. Only hostssl refuses one. So this check passing is necessary and not sufficient, and the result text says so.
How to fix it
# postgresql.conf
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server.crt'
ssl_key_file = '/etc/postgresql/ssl/server.key'
# Then in the host-based auth file, require it per rule
# Before
host all all 10.0.1.0/24 scram-sha-256
# After
hostssl all all 10.0.1.0/24 scram-sha-256
sudo systemctl restart postgresqlVerify a live session is actually encrypted:
psql -c "SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid()"Check 4: Is the PostgreSQL certificate about to expire?
Same failure mode as check 2, same 30-day window. The audit asks the running cluster for ssl_cert_file, resolves a relative path against the data directory, and runs the same openssl x509 -checkend 2592000 test.
How to check manually
CERT=$(sudo -u postgres psql -X -tAc 'SHOW ssl_cert_file')
sudo openssl x509 -checkend 2592000 -noout -in "$CERT" && echo "more than 30 days" || echo "under 30 days"| Result | When | What it means |
|---|---|---|
| PASS | The certificate is valid for more than 30 more days | No action needed yet. |
| WARN | The certificate expires within 30 days | Rotate it before TLS connections start failing. |
| SKIP | TLS is off so there is nothing to check, the ssl_cert_file path does not exist, or PostgreSQL is not accessible | All three reasons are reported separately. |
The first SKIP reason chains off check 3 deliberately: if ssl is off there is no certificate in use, so this check reports that rather than inventing a finding. Fix check 3 first and this one becomes meaningful.
How to fix it
sudo openssl req -new -x509 -days 365 -nodes \
-keyout /etc/postgresql/ssl/server.key \
-out /etc/postgresql/ssl/server.crt \
-subj "/CN=$(hostname)"
sudo chmod 600 /etc/postgresql/ssl/server.key
sudo chown postgres:postgres /etc/postgresql/ssl/server.key /etc/postgresql/ssl/server.crt
sudo systemctl restart postgresqlsslmode=require, PostgreSQL encrypts the connection but does not verify the certificate, so a self-signed one works and a man-in-the-middle also works. sslmode=verify-full is what actually authenticates the server, and it needs the client to trust your CA. Decide which of the two you are doing before generating certificates, because it changes what you have to distribute.Check 5: Does MongoDB require TLS?
MongoDB accepts unencrypted connections unless the TLS mode says otherwise. Its authentication uses SCRAM, so a password is not sent in the clear, but the handshake is still visible to anyone capturing traffic and gives an attacker enough to attempt an offline attack. Everything after authentication, meaning all your data, is plain text.
The audit reads mongod.conf for a TLS mode of requireTLS (or the older requireSSL), and if it does not find one, grades the result against bindIp.
How to check manually
sudo grep -E 'mode:[[:space:]]*(requireTLS|requireSSL)' /etc/mongod.conf
sudo grep -E '^[[:space:]]*bindIp' /etc/mongod.conf| Result | When | What it means |
|---|---|---|
| PASS | The TLS mode is requireTLS or requireSSL | All client connections must be encrypted. |
| FAIL | No required TLS mode, and bindIp reaches beyond loopback | Traffic crosses the network in clear text on an instance that is reachable off the host. |
| WARN | No required TLS mode, but bound to loopback only | Tolerable while nothing crosses a network. Set the mode before exposing it. |
| SKIP | MongoDB is not installed, or mongod.conf was not found | The two reasons are reported separately. |
Only requireTLS passes. The intermediate modes preferTLS and allowTLS accept plaintext connections, so as far as this check is concerned they are the same as having no TLS at all: whether they warn or fail depends entirely on bindIp, exactly as disabled would. That is stricter than MongoDB's own documentation frames it, and it is the right line, because a mode that permits plaintext will be used for plaintext by any client that does not opt in.
How to fix it
# /etc/mongod.conf
net:
tls:
mode: requireTLS
certificateKeyFile: /etc/mongodb/ssl/server.pem
CAFile: /etc/mongodb/ssl/ca.pem# MongoDB wants the cert and key in one file
cat server.crt server.key | sudo tee /etc/mongodb/ssl/server.pem > /dev/null
sudo chmod 600 /etc/mongodb/ssl/server.pem
sudo chown mongodb:mongodb /etc/mongodb/ssl/server.pem
sudo systemctl restart mongodThen update the connection strings: mongodb://user:pass@host:27017/db?tls=true&tlsCAFile=/path/to/ca.pem.
requireTLS disconnects every client at once. Set preferTLS first so both encrypted and plaintext connections work, roll out the client change, confirm nothing is still connecting in plain text, then switch to requireTLS. Just do not stop at preferTLS, which is what this check exists to catch.What this audit does not cover
Being explicit about scope is part of the point of publishing thresholds.
- Whether TLS is actually enforced per rule. Check 3 passing means PostgreSQL
sslis on, not thathostsslis used. Ahostline still accepts a plaintext connection. - Redis TLS. Out of scope by design. The network isolation audit covers binding it to loopback, which is the right answer for Redis.
- Certificate trust and validity beyond expiry. The checks test the expiry date only. A self-signed certificate, a wrong hostname, or a certificate from an untrusted CA all pass as long as the date is in range.
- Client-side verification. Whether your application uses
sslmode=verify-fullrather thanrequire, or pins a certificate, is invisible to a server-side check. - Encryption at rest. Data files and backups are a different layer. Backup encryption is in the hardening audit.
- Managed database services. RDS, Cloud SQL and Azure Database expose none of these variables to a host session, and typically enforce TLS at the provider level.
The manual audit problem at scale
Running these 5 checks by hand takes 10 to 15 minutes per server, and the tedious part is not the commands. It is that "is TLS on" has a different meaning in each engine: a variable that must be ON in MySQL, a setting that must be paired with a per-rule keyword in PostgreSQL, a mode with three permissive values in MongoDB. Answering the same question three ways for every server is how it stops getting answered.
The certificate half has a worse property: it is a clock. Every certificate on every server is quietly counting down, and the check only becomes interesting in the 30 days before it matters. A quarterly manual audit has a real chance of landing on either side of that window and telling you nothing useful, and the failure mode is not a weakened connection, it is an outage at a moment nobody chose.
That is the case for running this one on a schedule rather than on a whim, and for keeping the previous result. It is the same server management drift problem that compounds across every audit category, except here the drift is a date rather than a config.
How CtrlOps runs all 5 checks in one click
Instead of running these commands on every server by hand, CtrlOps runs the whole Transport Encryption audit over the SSH connection you already have open. Nothing is installed on the server: no agent, no daemon, no package, no new credentials. The checks are read-only POSIX shell that executes, streams its results back, and leaves nothing behind.
Watch it run
The recording below walks through the SSH & Access audit rather than this one, but the flow is identical for every audit in the catalog: pick it, watch the checks stream in, read the score, send the failures to the AI Terminal. The written steps that follow cover the same ground if you would rather read than watch.
Step 1: Open the Audit tab
Connect to your server in CtrlOps and open the Audit tab in the left sidebar.
Step 2: Select Transport Encryption
Choose it from the Database category. All 5 checks are listed with their descriptions, severity levels and the estimated run time (about 11 seconds in total). Toggle off any check you want to skip.
Step 3: Run it
The audit runs over your existing SSH session. Results stream in as they complete, so you watch each check pass, warn or fail rather than waiting on a final report.
Step 4: Read the report
You get a summary containing:
- A hardening score out of 100
- A severity breakdown: how many HIGH, MEDIUM and LOW findings
- Pass, warning, failed and skipped counts
- A findings table you can sort and filter
Step 5: Fix what failed
Select the failed findings and send them to the AI Terminal. Each proposed fix command appears with an explanation of what it changes. Nothing runs until you review and approve it, which matters here because enabling enforcement disconnects clients that are not ready for it.
Step 6: Re-run and compare
Run the same audit again after applying the fixes and watch the hardening score move. This is the audit to run on a schedule rather than on demand, because two of its five checks are counting down to a date.
| Task | By hand, 8 servers | CtrlOps, 8 servers |
|---|---|---|
| Run all 5 checks across 3 engines | About 1.5 hours | About 90 seconds |
| Catch a certificate inside its 30-day window | Only if you happen to check that month | Automatic |
| Get a scored report | Not available | Automatic |
| Export a PDF for a client | Not available | One click |
| Fix what failed | Copy-paste from your notes | AI-generated commands behind an approval gate |
Conclusion
Transport encryption is the layer that decides whether being on the network path is worth anything to an attacker. Without it, the credential your authentication audit made strong is readable in transit, and so is everything the query returns.
All five checks are MEDIUM because none opens a door on its own, and two of them are on a timer rather than a configuration: a certificate inside its 30-day window is a scheduled outage, not a vulnerability. The CtrlOps Security Audit runs the set in about 11 seconds per server, agentlessly, which makes a weekly check practical.
This completes the database category. The other five checklists cover authentication, credential storage, configuration and hardening, least-privilege permissions and network isolation.
Frequently asked questions
No. MySQL, PostgreSQL and MongoDB all accept unencrypted connections out of the box. MySQL may have TLS available while not requiring it, PostgreSQL defaults to ssl = off on many distributions, and MongoDB accepts plaintext unless a TLS mode is configured. Each one has to be enabled and then enforced separately, and the enabling is the easy half.
Available means the server can negotiate an encrypted connection if the client asks. Required means it refuses one that does not. Most client libraries default to not asking, so a server with TLS available and not required is, in practice, serving plaintext connections. That gap is what check 1 warns about and why require_secure_transport = ON is the setting that matters in MySQL.
It means they can be. A host line in the host-based authentication file still accepts an unencrypted connection from a client that does not request TLS. hostssl is what refuses one. So check 3 passing on ssl = on is necessary but not sufficient: change host to hostssl on every network-facing rule to actually enforce it.
Because preferTLS and allowTLS both accept plaintext connections. A mode that permits plaintext will be used for plaintext by any client that does not explicitly opt in, which makes it functionally the same as no TLS for the connections you were worried about. preferTLS is a useful migration step on the way to requireTLS; it is not a destination.
Clients that verify the certificate refuse to connect, so the application goes down at whatever moment the certificate happens to expire. Clients that do not verify keep connecting, which means the connection is encrypted but no longer authenticated and is open to a man-in-the-middle. Neither outcome is good, which is why the audit warns 30 days out rather than at expiry.
Because "no certificate configured" is check 1 or check 3 to fix, not this one. This check answers a single question: is the certificate in use about to expire. If there is no certificate, or the configured path does not exist on disk, there is nothing to date, so it reports SKIP and names the reason. A configured path that does not exist is the one worth investigating: it means TLS is not working despite appearing set up.
For a Unix socket connection, no: the traffic never touches a network interface, and MySQL with skip_networking on passes this audit for exactly that reason. For a TCP connection to 127.0.0.1 it is cheap insurance, because it costs almost nothing and protects you the day someone moves the application to another host and only changes the connection string.
It depends on what the client does with it. With sslmode=require PostgreSQL encrypts the connection and does not verify the certificate, so a self-signed one works and so does an attacker's. With sslmode=verify-full the client authenticates the server, which needs it to trust your CA. Decide which you are doing before generating anything, because it changes what you have to distribute to clients.
Because the right answer for Redis is isolation rather than encryption. Redis 6 and later support TLS, but a Redis instance should be bound to loopback, which removes the network path entirely and makes transport encryption moot. Check 5 of the network isolation audit covers the binding. If you do run Redis across a network, configure tls-port yourself, because nothing here will tell you whether you did.
It opens one administrative connection per engine over your existing SSH session, then reads require_secure_transport, have_ssl and skip_networking from MySQL, ssl and listen_addresses from PostgreSQL, and the TLS mode plus bindIp from the MongoDB config. For both certificate checks it resolves the configured path, including relative paths against the data directory, and runs openssl x509 -checkend against a 30-day window. Every check is read-only and the audit takes about 11 seconds.
It reads server variables and certificate files on engines you administer. It does not apply to managed services such as Amazon RDS, Google Cloud SQL or Azure Database, where TLS is normally enforced by the provider and none of these variables are reachable from a host session. It also does not audit Redis TLS or Kubernetes-orchestrated clusters.