Back to blog
AWSBest PracticesCloud SecurityDatabasesOperations & Compliance

SSL Not Enforced on RDS: Why It Matters and How to Fix It

Learn why unenforced SSL on RDS exposes credentials and data, and how to require TLS with force_ssl, require_secure_transport, Terraform, and OPA.

TL;DR

If your RDS parameter group does not set force_ssl (PostgreSQL/SQL Server) or require_secure_transport (MySQL/MariaDB), clients can connect in plaintext and credentials plus query data travel unencrypted. Flip the parameter to 1 on a custom parameter group, attach it, and verify every client uses TLS before you reboot.

Database connections carry some of the most sensitive traffic in your environment: login credentials, query payloads, and result sets. By default, Amazon RDS accepts both encrypted and unencrypted connections. That means a misconfigured or legacy client can happily talk to your database in cleartext, and nothing stops it. The SSL Not Enforced on RDS check (rds_ssl_not_required) flags instances whose parameter group does not require encryption in transit.


What this check detects

Lensix inspects the parameter group attached to each RDS instance and looks for the engine-specific parameter that forces TLS:

  • PostgreSQL and SQL Server: force_ssl must be set to 1
  • MySQL and MariaDB: require_secure_transport must be set to ON (or 1)

If the instance is using a default parameter group, or a custom group where these values are left at their default of 0/OFF, the check fails. The key detail is that RDS encryption in transit is opt-in at the server level. Enabling storage encryption at rest does nothing for connections, and neither does sitting inside a private subnet.

Note: Default RDS parameter groups cannot be edited. To enforce SSL you must create a custom parameter group, change the value there, and attach it to the instance. This is the most common reason the parameter is left unset: people assume the default group is configurable.


Why it matters

Without enforced TLS, a connection that is supposed to be encrypted can silently downgrade to plaintext. The application owner usually has no idea, because the connection still works fine. The risk shows up in a few concrete ways:

  • Credential theft. Database usernames and passwords are sent during the handshake. On an unencrypted connection, anyone who can observe traffic on the path between the client and the database reads them in the clear.
  • Data interception. Query parameters and result sets, including PII, payment data, and tokens, are readable to a network observer.
  • VPC is not a guarantee. A common assumption is that private networking makes encryption unnecessary. But a compromised EC2 instance, a misconfigured VPC peering, a mirrored ENI, or an attacker who has already gained a foothold can all sniff intra-VPC traffic.
  • Compliance failures. PCI DSS, HIPAA, and SOC 2 all expect encryption of sensitive data in transit. An RDS instance that accepts plaintext connections is a finding waiting to happen in your next audit.

The scenario that bites teams most often is gradual drift. SSL is enforced at launch, then someone spins up a quick reporting job or a new microservice without a TLS-aware driver config. Because RDS still accepts the plaintext connection, the new service works, ships, and now you have unencrypted traffic in production that no one flagged.

Warning: Enforcing SSL is a breaking change for any client that is not configured to use it. Before you set the parameter, confirm every application, ETL job, BI tool, and admin script connects with TLS. Flipping this on blind will drop connections from non-compliant clients the moment it takes effect.


How to fix it

The fix has three parts: create or edit a custom parameter group, set the enforcement parameter, and attach it to the instance. Some engines require a reboot for the change to apply.

Step 1: Create a custom parameter group (if you do not have one)

For PostgreSQL:

aws rds create-db-parameter-group \
  --db-parameter-group-name app-postgres-ssl \
  --db-parameter-group-family postgres15 \
  --description "Enforce SSL for PostgreSQL"

For MySQL:

aws rds create-db-parameter-group \
  --db-parameter-group-name app-mysql-ssl \
  --db-parameter-group-family mysql8.0 \
  --description "Enforce TLS for MySQL"

Step 2: Set the enforcement parameter

PostgreSQL and SQL Server use force_ssl:

aws rds modify-db-parameter-group \
  --db-parameter-group-name app-postgres-ssl \
  --parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=pending-reboot"

MySQL and MariaDB use require_secure_transport:

aws rds modify-db-parameter-group \
  --db-parameter-group-name app-mysql-ssl \
  --parameters "ParameterName=require_secure_transport,ParameterValue=ON,ApplyMethod=immediate"

Note: The PostgreSQL parameter on RDS is namespaced as rds.force_ssl. It is a static parameter, so ApplyMethod must be pending-reboot and the change only takes effect after the next reboot. The MySQL parameter is dynamic and applies immediately.

Step 3: Attach the parameter group to the instance

aws rds modify-db-instance \
  --db-instance-identifier my-app-db \
  --db-parameter-group-name app-postgres-ssl \
  --apply-immediately

Danger: Using --apply-immediately on a static parameter triggers a reboot, which means a brief outage. For Multi-AZ instances the failover usually completes in under a minute, but Single-AZ instances are unavailable until the reboot finishes. Schedule this during a maintenance window and drop --apply-immediately if you want it deferred to the next window.

Step 4: Confirm clients use TLS, then reboot

For PostgreSQL, append SSL mode to your connection string and supply the RDS CA bundle:

psql "host=my-app-db.abc123.us-east-1.rds.amazonaws.com \
  port=5432 dbname=app user=appuser \
  sslmode=verify-full sslrootcert=global-bundle.pem"

For MySQL:

mysql -h my-app-db.abc123.us-east-1.rds.amazonaws.com \
  --ssl-ca=global-bundle.pem --ssl-mode=VERIFY_IDENTITY \
  -u appuser -p

Download the current global CA bundle from AWS:

curl -o global-bundle.pem \
  https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

Verify enforcement is active

After the reboot, connect to PostgreSQL and check that SSL is in use:

SELECT ssl, version
FROM pg_stat_ssl
JOIN pg_stat_activity USING (pid)
WHERE pid = pg_backend_pid();

For MySQL, confirm a non-TLS connection is now rejected:

SHOW VARIABLES LIKE 'require_secure_transport';
-- Should return ON

How to prevent it from happening again

Manual remediation fixes the instance in front of you. To stop the misconfiguration from coming back, push enforcement into the layer that creates the databases.

Terraform

Define the parameter group in code and reference it from the instance so the setting is never default:

resource "aws_db_parameter_group" "postgres_ssl" {
  name   = "app-postgres-ssl"
  family = "postgres15"

  parameter {
    name         = "rds.force_ssl"
    value        = "1"
    apply_method = "pending-reboot"
  }
}

resource "aws_db_instance" "app" {
  identifier           = "my-app-db"
  engine               = "postgres"
  engine_version       = "15.4"
  instance_class       = "db.t3.medium"
  parameter_group_name = aws_db_parameter_group.postgres_ssl.name
  # ... other config
}

Policy-as-code gate

Catch the bad value in CI before it ships. This Open Policy Agent rule fails any plan that creates a PostgreSQL parameter group without force_ssl = 1:

package rds.ssl

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "aws_db_parameter_group"
  param := rc.change.after.parameter[_]
  param.name == "rds.force_ssl"
  param.value != "1"
  msg := sprintf("force_ssl must be 1 on %s", [rc.address])
}

Tip: Pair the policy gate with a continuous Lensix scan. CI catches new resources at deploy time, but it cannot see drift from out-of-band changes, like someone reattaching a default parameter group through the console. A scheduled scan closes that gap and alerts you when an instance stops enforcing TLS.


Best practices

  • Enforce at the server, verify at the client. Setting force_ssl stops plaintext connections, but it does not prevent man-in-the-middle attacks unless clients verify the certificate. Use sslmode=verify-full (PostgreSQL) or VERIFY_IDENTITY (MySQL) with the RDS CA bundle.
  • Pin the global CA bundle, not a regional one. AWS rotates the regional certificates on a schedule. The global bundle covers all regions and includes the certificates you need across rotations, which means fewer surprise outages when a CA expires.
  • Track CA expiry. RDS certificate authorities have expiry dates. Set a reminder ahead of the rotation deadline so you update client trust stores before connections start failing.
  • Apply the same rule to read replicas and clones. Replicas inherit the parameter group, but a clone or restored snapshot may default back. Treat every new instance as in-scope for the check.
  • Combine with encryption at rest. TLS protects data in transit; KMS-backed storage encryption protects it at rest. You want both, and most compliance frameworks expect both.
  • Roll out enforcement in stages. Enable SSL on non-prod first, fix any client that breaks, then promote the change to production during a maintenance window.

Enforcing TLS on RDS is one of the cheapest, highest-leverage security wins available. It costs nothing extra, takes minutes to apply, and removes an entire class of credential and data exposure. The only real work is making sure your clients are ready, so get that confirmed and then make the parameter mandatory everywhere.