SSL/TLS Certificates on Linux
TLS (Transport Layer Security), the successor to SSL, encrypts data in transit between clients and servers. Properly configured TLS prevents eavesdropping, tampering, and impersonation. Misconfigured TLS, on the other hand, can create a false sense of security while leaving data exposed to man-in-the-middle attacks. This guide covers generating certificates with OpenSSL, obtaining free certificates from Let's Encrypt, managing certificate chains, hardening TLS configuration, and troubleshooting with command-line tools.
Hub: Linux Security Hardening | See also: Encryption Guide, Server Checklist
Self-Signed Certificates with OpenSSL
Self-signed certificates are useful for development, internal services, and testing. They provide encryption but are not trusted by browsers without an explicit exception because no certificate authority has vouched for them.
# Generate a private key and self-signed certificate in one step
openssl req -new -x509 -days 365 -nodes \
-keyout /etc/ssl/private/mysite.key \
-out /etc/ssl/certs/mysite.crt \
-subj "/C=US/ST=State/L=City/O=Org/CN=mysite.example.com"
# Verify the certificate contents
openssl x509 -in /etc/ssl/certs/mysite.crt -noout -text
# Check just the subject and expiry
openssl x509 -in /etc/ssl/certs/mysite.crt -noout -subject -dates
# Generate a certificate with Subject Alternative Names inline
openssl req -new -x509 -days 365 -nodes \
-keyout mysite.key -out mysite.crt \
-subj "/CN=mysite.example.com" \
-addext "subjectAltName=DNS:mysite.example.com,DNS:www.mysite.example.com"
The -nodes flag means the private key is not encrypted with a passphrase,
which is common for automated services that must start without human
intervention. For higher security, omit -nodes and provide a passphrase,
then configure your web server to prompt for it at startup.
Set appropriate file permissions on the private key so that only the web server process can read it:
sudo chmod 600 /etc/ssl/private/mysite.key
sudo chown root:root /etc/ssl/private/mysite.key
Certificate Signing Requests (CSR)
When obtaining a certificate from a public certificate authority, you generate a CSR that contains your public key and identity information. The CA verifies your identity, signs the CSR, and returns a certificate.
# Generate a 4096-bit RSA private key
openssl genrsa -out mysite.key 4096
# Or generate an ECDSA key (smaller, faster, equally secure)
openssl ecparam -genkey -name prime256v1 -out mysite-ec.key
# Generate a CSR from the RSA key
openssl req -new -key mysite.key -out mysite.csr \
-subj "/C=US/ST=State/L=City/O=Org/CN=mysite.example.com"
# Verify the CSR contents
openssl req -in mysite.csr -noout -text -verify
# For Subject Alternative Names (multiple domains), use a config file:
cat > san.cnf <<'EOF'
[req]
distinguished_name = req_dn
req_extensions = v3_req
prompt = no
[req_dn]
CN = mysite.example.com
O = Example Org
C = US
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = mysite.example.com
DNS.2 = www.mysite.example.com
DNS.3 = api.mysite.example.com
EOF
openssl req -new -key mysite.key -out mysite.csr -config san.cnf
Modern browsers require the Subject Alternative Name extension. A certificate with only a Common Name (CN) will produce warnings in Chrome and Firefox. Always include SAN entries even if you have only one domain.
Understanding Certificate Chains
A certificate chain links your server certificate to a trusted root CA through one or more intermediate certificates. When configuring a web server, you must provide the full chain so that clients can verify trust all the way up to a root they already know:
Root CA (in browser/OS trust store, not sent by server)
-> Intermediate CA (sent by server, provided by your CA)
-> Your server certificate (sent by server)
# Concatenate your certificate and the intermediate(s) into a full chain
cat mysite.crt intermediate.crt > fullchain.pem
# If there are multiple intermediates, concatenate in order
cat mysite.crt intermediate1.crt intermediate2.crt > fullchain.pem
# Verify the chain against the system trust store
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
# Verify with an explicit intermediate
openssl verify -untrusted intermediate.crt mysite.crt
If the chain is incomplete, clients will see "unable to verify the first certificate" or "certificate not trusted" errors. This is one of the most common TLS deployment mistakes.
Let's Encrypt with Certbot
Let's Encrypt provides free, automated, and publicly trusted certificates. Certbot is the recommended ACME client for obtaining and renewing them.
# Install certbot
sudo apt install certbot # Debian/Ubuntu
sudo dnf install certbot # Fedora/RHEL
sudo apt install python3-certbot-nginx # Nginx plugin (optional)
# Standalone mode (certbot starts its own web server on port 80)
# Port 80 must be free and reachable from the internet
sudo certbot certonly --standalone -d mysite.example.com
# Webroot mode (use an existing web server's document root)
sudo certbot certonly --webroot -w /var/www/html -d mysite.example.com
# DNS challenge (useful when port 80 is blocked or for wildcards)
sudo certbot certonly --manual --preferred-challenges dns \
-d "*.mysite.example.com" -d mysite.example.com
# Certificates are stored in /etc/letsencrypt/live/mysite.example.com/
# fullchain.pem -- certificate + intermediates (use this in your server)
# privkey.pem -- private key
# cert.pem -- server certificate only
# chain.pem -- intermediate certificates only
Automatic Renewal
Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer or cron job to renew automatically:
# Test renewal without making changes
sudo certbot renew --dry-run
# Force immediate renewal (ignores the "not due yet" check)
sudo certbot renew --force-renewal
# Check the systemd timer
systemctl list-timers | grep certbot
# To reload your web server after renewal, add a deploy hook:
sudo certbot renew --deploy-hook "systemctl reload nginx"
Add the deploy hook permanently in
/etc/letsencrypt/renewal/mysite.example.com.conf:
[renewalparams]
deploy_hook = systemctl reload nginx
Without a deploy hook, the new certificate sits on disk but the web server continues using the old one in memory until it is restarted.
Inspecting Remote Certificates
The openssl s_client command connects to a remote server and displays its
certificate information. This is invaluable for troubleshooting chain issues,
expiry dates, and protocol support:
# Connect and show the full certificate chain
openssl s_client -connect mysite.example.com:443 -showcerts </dev/null
# Check the expiry date
echo | openssl s_client -connect mysite.example.com:443 2>/dev/null \
| openssl x509 -noout -dates
# Show the subject and issuer
echo | openssl s_client -connect mysite.example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer
# Check which cipher suite is negotiated
openssl s_client -connect mysite.example.com:443 -brief </dev/null
# Test a specific TLS version
openssl s_client -connect mysite.example.com:443 -tls1_3 </dev/null
# Test against a specific SNI hostname
openssl s_client -connect mysite.example.com:443 \
-servername mysite.example.com </dev/null
Hardening TLS Configuration
Modern TLS configuration should disable old protocols (SSLv3, TLS 1.0, TLS 1.1) and weak cipher suites. For Nginx, a hardened configuration looks like this:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
For Apache, equivalent directives apply in the VirtualHost block:
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
SSLHonorCipherOrder on
Header always set Strict-Transport-Security "max-age=63072000"
Use the Mozilla SSL Configuration Generator to produce settings tailored to your web server and desired compatibility level (Modern, Intermediate, Old).
Monitoring Certificate Expiry
Automate expiry checks so you are never caught off guard:
#!/usr/bin/env bash
# check-cert-expiry.sh -- run daily from cron
DOMAINS="mysite.example.com api.example.com shop.example.com"
WARN_DAYS=14
for DOMAIN in $DOMAINS; do
EXPIRY=$(echo | openssl s_client -connect "$DOMAIN:443" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
echo "WARNING: $DOMAIN certificate expires in $DAYS_LEFT days"
fi
done
Run this script from cron daily and pipe the output to your alerting system. Certificates should never expire unexpectedly in a well-managed environment. An expired certificate causes immediate, visible downtime for every user.