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
sudoprivileges. - A registered domain name with an
Arecord pointing to the server's public IP. This guide usesyour_domainas a placeholder. - Nginx installed and serving a site for
your_domainon port 80 (sudo apt install nginx). - MySQL 8.0 installed from the Ubuntu repositories (
sudo apt install mysql-server) and an application user, calledapp_userhere, 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;
}
}
WarningOnce a browser has seen the HSTS header it refuses plain HTTP for your domain for the whole
max-ageperiod. Only add it when HTTPS works for every subdomain you serve, and do not addincludeSubDomainsunless that is true for all of them.
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
ImportantKeep your current SSH session open and test a new connection from a second terminal before you log out. If the new connection fails, you can still fix the file from the open session.
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 connectorConnection refused: Let's Encrypt must reach port 80 on your server. Check that theArecord points to the right IP (dig +short your_domain) and thatsudo ufw statusshowsNginx Fullallowed. - 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/*.pemare owned bymysqland reviewsudo journalctl -u mysql. - Old SSH clients fail with
no matching cipher foundorno 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.
