Encryption in transit protects data while it travels between clients and your server, so nobody on the network path can read or modify it. Most compliance frameworks (PCI DSS, HIPAA, GDPR's "appropriate technical measures") expect it for any traffic that carries credentials or personal data. In this tutorial you will secure the three channels a typical Ubuntu 24.04 server exposes: HTTPS on Nginx with a Let's Encrypt certificate, TLS-only connections to MySQL, and SSH restricted to modern ciphers. After each change you will verify from the outside that the encryption is actually in place.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges.
  • A registered domain name with an A record pointing to the server's public IP. This guide uses your_domain as a placeholder.
  • Nginx installed and serving a site for your_domain on port 80 (sudo apt install nginx).
  • MySQL 8.0 installed from the Ubuntu repositories (sudo apt install mysql-server) and an application user, called app_user here, if you want to follow Step 4.
  • UFW allowing SSH, HTTP and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Step 1 - Obtaining a TLS certificate for Nginx

Let's Encrypt issues free, browser-trusted certificates, and Certbot's Nginx plugin requests the certificate and edits your server block for you. Install Certbot and the plugin from the Ubuntu repositories:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Request a certificate for your domain. The --redirect flag makes Certbot add a permanent redirect from HTTP to HTTPS, so no request is ever served in plain text:

sudo certbot --nginx --redirect -d your_domain -d www.your_domain

Certbot asks for an email address for expiry notices and for acceptance of the terms of service. When it finishes you see a message like this:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/your_domain/privkey.pem
...
Successfully deployed certificate for your_domain to /etc/nginx/sites-enabled/your_domain

Certificates are valid for 90 days. The certbot package installs a systemd timer that renews them automatically. Confirm the timer is active and that a renewal would succeed:

systemctl list-timers certbot.timer
sudo certbot renew --dry-run

The dry run should end with Congratulations, all simulated renewals succeeded.

Step 2 - Hardening the Nginx TLS configuration

Certbot's server block includes /etc/letsencrypt/options-ssl-nginx.conf, which already limits the protocols to TLS 1.2 and TLS 1.3 and sets a reasonable cipher list. What it does not add is HTTP Strict Transport Security (HSTS), which tells browsers to never try plain HTTP for your domain again, even if a user types http://.

Open the server block that Certbot modified:

sudo nano /etc/nginx/sites-available/your_domain

Inside the server block that contains listen 443 ssl, add the HSTS header below the Certbot lines:

server {
    server_name your_domain www.your_domain;
    root /var/www/your_domain;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    add_header Strict-Transport-Security "max-age=31536000" always;

    location / {
        try_files $uri $uri/ =404;
    }
}

Test the configuration and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

nginx -t should report syntax is ok and test is successful.

Step 3 - Verifying HTTPS from the outside

Run these checks from your local machine, not from the server, so they follow the same path as a real visitor.

First confirm that plain HTTP redirects to HTTPS:

curl -sI http://your_domain | head -n 3
HTTP/1.1 301 Moved Permanently
Server: nginx/1.24.0 (Ubuntu)
Date: ...

Then check that the HTTPS response carries the HSTS header:

curl -sI https://your_domain | grep -i strict-transport
strict-transport-security: max-age=31536000

Check which protocol and cipher are negotiated:

openssl s_client -connect your_domain:443 -servername your_domain -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = your_domain
Verification: OK
...

To list every protocol and cipher the server accepts, use the ssl-enum-ciphers script from Nmap (sudo apt install nmap on Ubuntu):

nmap --script ssl-enum-ciphers -p 443 your_domain

The output should only contain TLSv1.2 and TLSv1.3 sections, and every cipher should be graded A. If you see TLSv1.0 or TLSv1.1, another server block or include is overriding the protocols; search for it with grep -rn ssl_protocols /etc/nginx/.

Step 4 - Requiring TLS for MySQL connections

When an application connects to MySQL over the network, queries and results travel in plain text unless the connection uses TLS. MySQL 8.0 on Ubuntu generates a self-signed CA and server certificate in /var/lib/mysql/ on first start, so TLS is available but not enforced. Confirm it is available:

sudo mysql -e "SHOW VARIABLES LIKE 'have_ssl';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| have_ssl      | YES   |
+---------------+-------+

To reject any TCP connection that does not use TLS, edit the server configuration:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

Add these lines at the end of the [mysqld] section:

require_secure_transport = ON
tls_version = TLSv1.2,TLSv1.3

If remote clients need to reach the database, also change bind-address in the same file from 127.0.0.1 to the private IP the clients use. Restart MySQL to apply the change:

sudo systemctl restart mysql

Connections through the local Unix socket are still accepted, because MySQL treats the socket as a secure transport. TCP clients must now use TLS. You can additionally require it per account, which documents the requirement in the user definition:

sudo mysql -e "ALTER USER 'app_user'@'%' REQUIRE SSL;"

Adjust the host part ('%') to match how the account was created.

Verifying the MySQL connection

From a client host with the mysql-client package, connect over TCP. --ssl-mode=REQUIRED encrypts the connection; to also verify the server certificate, copy /var/lib/mysql/ca.pem from the server to the client and use --ssl-mode=VERIFY_CA --ssl-ca=ca.pem instead:

mysql -h your_server_ip -u app_user -p --ssl-mode=REQUIRED -e "\s" | grep -i ssl
SSL:			Cipher in use is TLS_AES_256_GCM_SHA384

Now confirm that an unencrypted connection is refused:

mysql -h your_server_ip -u app_user -p --ssl-mode=DISABLED
ERROR 3159 (HY000): Connections using insecure transport are prohibited while --require_secure_transport=ON.

Most client libraries (PDO, mysqlclient, the MySQL Connector drivers) have an equivalent SSL mode or CA option. Set it in your application's database settings so it verifies the CA rather than only encrypting.

Step 5 - Restricting SSH to modern algorithms

SSH is always encrypted, but the server still accepts older key exchange, cipher and MAC algorithms for compatibility. OpenSSH 9.6 on Ubuntu 24.04 has good defaults; restricting them explicitly makes the policy auditable and removes CBC ciphers and SHA-1 based MACs from the list.

First check what the server currently offers:

sudo sshd -T | grep -E '^(ciphers|macs|kexalgorithms)'

Ubuntu reads extra settings from /etc/ssh/sshd_config.d/. Create a drop-in file so that package upgrades never overwrite your changes:

sudo nano /etc/ssh/sshd_config.d/10-crypto.conf
KexAlgorithms [email protected],curve25519-sha256,[email protected]
Ciphers [email protected],[email protected],[email protected],aes256-ctr,aes128-ctr
MACs [email protected],[email protected],[email protected]
PasswordAuthentication no

PasswordAuthentication no forces key-based logins. Only include it if your SSH key already works for your sudo user.

Validate the configuration and reload the service:

sudo sshd -t
sudo systemctl reload ssh

From your local machine, confirm which algorithms were negotiated:

ssh -v your_user@your_server_ip exit 2>&1 | grep -E 'kex: algorithm|kex: server->client'
debug1: kex: algorithm: [email protected]
debug1: kex: server->client cipher: [email protected] MAC: <implicit> compression: none

Troubleshooting

  • Certbot fails with Timeout during connect or Connection refused: Let's Encrypt must reach port 80 on your server. Check that the A record points to the right IP (dig +short your_domain) and that sudo ufw status shows Nginx Full allowed.
  • MySQL client error SSL connection error: SSL is required but the server doesn't support it: the server is not loading its certificates. Check the files in /var/lib/mysql/*.pem are owned by mysql and review sudo journalctl -u mysql.
  • Old SSH clients fail with no matching cipher found or no matching key exchange method found: the client is too old for the restricted list. Update the client rather than re-enabling legacy algorithms on the server.

Conclusion

Your web traffic now uses TLS 1.2 or 1.3 with automatic certificate renewal and HSTS, MySQL rejects any TCP connection without TLS, and SSH negotiates only modern algorithms. Each setting was verified from the client side, which is the evidence an auditor will ask for. As next steps, move traffic between your own servers onto a private network or a WireGuard tunnel, enable TLS on any other service that listens on a public port (mail, Redis, message brokers), and add certificate expiry to your monitoring.