Moving SSL/TLS certificates between servers is a common task during migrations, load balancer changes, or disaster recovery. However, certificates that worked on the original server may produce errors on the new one. This guide covers the most common SSL errors you will encounter during migration and how to resolve them.
Error: Certificate Chain Is Incomplete
This is the most frequent issue after migrating certificates. Browsers may show "Your connection is not private" or "Certificate not trusted," even though the certificate itself is valid.
Cause: The intermediate certificate(s) were not copied to the new server. Most SSL certificates are signed by an intermediate CA, not the root CA directly. The server must send the full chain (server cert + intermediate certs) to the client.
How to fix:
# Check the chain with openssl:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
# Look for this in the output:
# verify error:num=21:unable to verify the first certificate
# This means the chain is incomplete.
# Get the correct chain from your CA or use:
# For Let's Encrypt, the chain file is usually fullchain.pem (cert + intermediate)
Apache Configuration
# In your VirtualHost:
SSLCertificateFile /etc/ssl/certs/yourdomain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
# Or in Apache 2.4.8+, you can concatenate them:
SSLCertificateFile /etc/ssl/certs/yourdomain-fullchain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
Nginx Configuration
# Nginx requires a single file with the full chain:
# Concatenate: server cert first, then intermediate(s)
cat yourdomain.crt intermediate.crt > yourdomain-fullchain.crt
# In your server block:
ssl_certificate /etc/ssl/certs/yourdomain-fullchain.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
Error: Private Key Does Not Match Certificate
You may see errors like "SSL_CTX_use_PrivateKey_file ... error" in Apache, or "SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch" in Nginx.
Cause: The private key file on the new server does not correspond to the certificate. This happens when the wrong key file was copied, or the files got mixed up between multiple domains.
How to verify the match:
# Compare the modulus of the certificate and key:
openssl x509 -noout -modulus -in yourdomain.crt | openssl md5
openssl rsa -noout -modulus -in yourdomain.key | openssl md5
# If the MD5 hashes match, the key and cert are paired.
# If they differ, you have mismatched files.
# You can also check the CSR:
openssl req -noout -modulus -in yourdomain.csr | openssl md5
Fix: Locate the correct private key that was used to generate the CSR for this certificate. If you cannot find it, you will need to generate a new CSR and reissue the certificate from your CA.
Error: Wrong File Permissions
After copying files to the new server, incorrect permissions can cause the web server to fail to start or refuse to load the certificate.
Correct permissions:
# Certificate files (public — can be world-readable):
chmod 644 /etc/ssl/certs/yourdomain.crt
chmod 644 /etc/ssl/certs/intermediate.crt
chown root:root /etc/ssl/certs/yourdomain.crt
# Private key (must be restricted):
chmod 600 /etc/ssl/private/yourdomain.key
chown root:root /etc/ssl/private/yourdomain.key
# The private key directory:
chmod 700 /etc/ssl/private/
If the private key is world-readable, some web servers will refuse to use it, and it is a serious security issue. The key should only be readable by root (or the specific user the web server runs as).
Error: SSL Handshake Failure
The client connects but the TLS handshake fails. This can manifest as "ERR_SSL_PROTOCOL_ERROR" in browsers or "SSL routines:ssl3_get_record:wrong version number" in openssl.
Common causes:
- The server is not listening on port 443 or the SSL VirtualHost is misconfigured.
- A non-SSL VirtualHost is responding on port 443, sending plain HTTP.
- Protocol mismatch — the server only supports TLS versions the client does not.
Debugging with openssl:
# Test the connection and see the full handshake:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
# Test with a specific TLS version:
openssl s_client -connect yourdomain.com:443 -tls1_2
openssl s_client -connect yourdomain.com:443 -tls1_3
# Check which protocols and ciphers the server supports:
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
Testing the Complete SSL Configuration
After migration, run a comprehensive test:
# Full openssl test (shows chain, protocol, cipher):
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -showcerts
# Check certificate dates:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | \
openssl x509 -noout -dates
# Verify the chain locally:
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt yourdomain-fullchain.crt
You should also test from an external perspective using an online SSL checker, which will verify the chain, protocols, and cipher configuration as seen by clients worldwide.
Let's Encrypt Certificate Migration
If you are using Let's Encrypt certificates, there are specific considerations for migration:
Option 1: Copy the Existing Certificate
# Copy the entire Let's Encrypt directory:
# On the old server:
tar czf letsencrypt-backup.tar.gz /etc/letsencrypt/
# On the new server:
tar xzf letsencrypt-backup.tar.gz -C /
# Install certbot on the new server and test renewal:
certbot renew --dry-run
Option 2: Issue a New Certificate (Recommended)
# Install certbot on the new server, update DNS to point to it, then:
certbot certonly --nginx -d yourdomain.com -d www.yourdomain.com
# Or for Apache:
certbot certonly --apache -d yourdomain.com -d www.yourdomain.com
Issuing a new certificate on the new server is generally cleaner than migrating. It ensures the renewal configuration is correct for the new server's environment.
Apache vs Nginx: Key Configuration Differences
| Feature | Apache | Nginx |
|---|---|---|
| Certificate file | SSLCertificateFile |
ssl_certificate |
| Private key | SSLCertificateKeyFile |
ssl_certificate_key |
| Chain file | SSLCertificateChainFile (separate) |
Concatenated into ssl_certificate |
| Test config | apache2ctl configtest |
nginx -t |
| Reload | systemctl reload apache2 |
systemctl reload nginx |
For a detailed guide on setting up Nginx with SSL from scratch, see Nginx, PHP, and SSL on Ubuntu. For setting up security headers like HSTS on your new server, see HTTP Strict Transport Security (HSTS).
Migration Checklist
- Copy all certificate files: server cert, private key, intermediate/chain cert.
- Verify the key matches the certificate using
opensslmodulus comparison. - Set correct file permissions (644 for certs, 600 for keys).
- Configure the web server with the correct file paths.
- Test the configuration before reloading (
apache2ctl configtestornginx -t). - Test with
openssl s_clientto verify the chain is complete. - Verify certificate renewal (cron/certbot timer) is configured on the new server.
- Test from a browser and an external SSL checker.