Manual SSL/TLS Configuration with Own Certificates

Introduction

While Let's Encrypt provides free automated SSL/TLS certificates, there are scenarios where you need to use your own certificates. Organizations may require Extended Validation (EV) or Organization Validation (OV) certificates for enhanced trust, internal Certificate Authorities (CA) for private networks, wildcard certificates from commercial providers, or certificates with longer validity periods for specific business requirements.

Manual SSL/TLS configuration gives you complete control over certificate generation, signing, and deployment. This approach is essential for enterprise environments, development/testing setups with self-signed certificates, multi-year commercial certificates, internal infrastructure not accessible from the internet, and compliance requirements mandating specific certificate authorities.

This comprehensive guide covers everything you need to know about manually configuring SSL/TLS certificates. You'll learn how to generate Certificate Signing Requests (CSR), create self-signed certificates for testing, install commercial SSL certificates, configure Apache and Nginx for HTTPS, implement certificate chains correctly, troubleshoot common certificate issues, and follow security best practices. Whether you're setting up a development environment or deploying enterprise-grade SSL infrastructure, this guide provides the foundation for successful manual certificate management.

Prerequisites

Before beginning manual SSL/TLS configuration, ensure you have:

  • Apache or Nginx installed and running on your Linux server
  • Root or sudo access to your server
  • OpenSSL installed (pre-installed on most Linux distributions)
  • Basic understanding of SSL/TLS concepts and Public Key Infrastructure (PKI)
  • A registered domain name (for production certificates)
  • Firewall configured to allow HTTPS traffic on port 443
  • Text editor knowledge for configuration file editing
  • Understanding of your web server's virtual host or server block configuration

Understanding SSL/TLS Certificates

Certificate Components

An SSL/TLS certificate contains:

Subject Information:

  • Common Name (CN): Domain name (e.g., www.example.com)
  • Organization (O): Company name
  • Organizational Unit (OU): Department
  • Locality (L): City
  • State/Province (ST): State
  • Country (C): Two-letter country code

Technical Elements:

  • Public Key: Used for encryption
  • Private Key: Must be kept secure, used for decryption
  • Signature: CA's digital signature
  • Validity Period: Start and expiration dates
  • Serial Number: Unique identifier
  • Extensions: Subject Alternative Names (SAN), Key Usage, etc.

Certificate Types

By Validation Level:

  • Domain Validation (DV): Verifies domain ownership only
  • Organization Validation (OV): Verifies domain and organization
  • Extended Validation (EV): Highest validation, shows organization in browser

By Coverage:

  • Single Domain: Covers one fully qualified domain name
  • Wildcard: Covers domain and all subdomains (*.example.com)
  • Multi-Domain (SAN): Covers multiple specific domains

By Purpose:

  • Self-Signed: For testing/development
  • Internal CA: For private networks
  • Commercial CA: For public-facing websites

Installing OpenSSL

OpenSSL is typically pre-installed, but verify:

# Check OpenSSL version
openssl version

# Expected output:
# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

# If not installed:
# Ubuntu/Debian
sudo apt update
sudo apt install openssl -y

# CentOS/Rocky/AlmaLinux
sudo yum install openssl -y  # or dnf

Creating Self-Signed Certificates

Self-signed certificates are perfect for development and testing environments.

Generate Self-Signed Certificate (Single Command)

# Create directory for certificates
sudo mkdir -p /etc/ssl/private
sudo mkdir -p /etc/ssl/certs

# Generate self-signed certificate (valid for 365 days)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/example.com.key \
  -out /etc/ssl/certs/example.com.crt \
  -subj "/C=US/ST=California/L=San Francisco/O=Example Inc/OU=IT/CN=example.com"

Parameters explained:

  • req: Certificate request command
  • -x509: Create self-signed certificate
  • -nodes: Don't encrypt private key
  • -days 365: Valid for 365 days
  • -newkey rsa:2048: Generate 2048-bit RSA key
  • -keyout: Private key output file
  • -out: Certificate output file
  • -subj: Subject information (non-interactive)

Generate Self-Signed Certificate (Interactive)

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/example.com.key \
  -out /etc/ssl/certs/example.com.crt

You'll be prompted for:

Country Name (2 letter code) [AU]: US
State or Province Name (full name) [Some-State]: California
Locality Name (eg, city) []: San Francisco
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Example Inc
Organizational Unit Name (eg, section) []: IT Department
Common Name (e.g. server FQDN or YOUR name) []: example.com
Email Address []: [email protected]

Wildcard Self-Signed Certificate

# Create wildcard certificate for *.example.com
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/wildcard.example.com.key \
  -out /etc/ssl/certs/wildcard.example.com.crt \
  -subj "/C=US/ST=California/L=San Francisco/O=Example Inc/CN=*.example.com"

Multi-Domain (SAN) Self-Signed Certificate

Create OpenSSL config file:

sudo nano /tmp/san.cnf

Add:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = US
ST = California
L = San Francisco
O = Example Inc
OU = IT Department
CN = example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = blog.example.com
DNS.4 = api.example.com

Generate certificate:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/example.com.key \
  -out /etc/ssl/certs/example.com.crt \
  -config /tmp/san.cnf \
  -extensions v3_req

Secure Private Key

Set appropriate permissions:

# Private key should be readable only by root
sudo chmod 600 /etc/ssl/private/example.com.key

# Certificate can be world-readable
sudo chmod 644 /etc/ssl/certs/example.com.crt

# Verify permissions
ls -la /etc/ssl/private/example.com.key
ls -la /etc/ssl/certs/example.com.crt

Commercial Certificates: CSR Generation

For commercial SSL certificates, generate a Certificate Signing Request (CSR).

Generate Private Key and CSR

# Generate private key (2048-bit RSA)
sudo openssl genrsa -out /etc/ssl/private/example.com.key 2048

# Generate CSR
sudo openssl req -new \
  -key /etc/ssl/private/example.com.key \
  -out /tmp/example.com.csr \
  -subj "/C=US/ST=California/L=San Francisco/O=Example Inc/OU=IT/CN=example.com/[email protected]"

Generate CSR Interactively

sudo openssl req -new \
  -key /etc/ssl/private/example.com.key \
  -out /tmp/example.com.csr

Answer prompts carefully (information will appear in certificate):

Country Name (2 letter code) [AU]: US
State or Province Name (full name) [Some-State]: California
Locality Name (eg, city) []: San Francisco
Organization Name (eg, company) []: Example Inc
Organizational Unit Name (eg, section) []: IT Department
Common Name (e.g. server FQDN or YOUR name) []: example.com
Email Address []: [email protected]

Please enter the following 'extra' attributes:
A challenge password []: [Leave blank]
An optional company name []: [Leave blank]

Generate CSR for Wildcard Certificate

sudo openssl req -new \
  -key /etc/ssl/private/wildcard.example.com.key \
  -out /tmp/wildcard.example.com.csr \
  -subj "/C=US/ST=California/L=San Francisco/O=Example Inc/CN=*.example.com"

Generate CSR for Multi-Domain (SAN) Certificate

Create config file:

sudo nano /tmp/san_csr.cnf

Add:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = US
ST = California
L = San Francisco
O = Example Inc
OU = IT Department
CN = example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = blog.example.com
DNS.4 = api.example.com
DNS.5 = shop.example.com

Generate CSR:

# Generate private key first
sudo openssl genrsa -out /etc/ssl/private/example.com.key 2048

# Generate CSR with SAN
sudo openssl req -new \
  -key /etc/ssl/private/example.com.key \
  -out /tmp/example.com.csr \
  -config /tmp/san_csr.cnf

View CSR Contents

# View CSR details
sudo openssl req -text -noout -in /tmp/example.com.csr

# View subject alternative names
sudo openssl req -text -noout -in /tmp/example.com.csr | grep -A 5 "Subject Alternative Name"

Submit CSR to Certificate Authority

  1. Copy CSR content:
cat /tmp/example.com.csr
  1. Submit to CA (Digicert, Sectigo, GlobalSign, etc.)
  2. Complete domain validation (email, DNS, or HTTP)
  3. Download signed certificate from CA
  4. Save certificate as /etc/ssl/certs/example.com.crt

Installing Commercial SSL Certificates

Certificate Chain Structure

Commercial certificates come with:

  • Primary Certificate: Your domain certificate (example.com.crt)
  • Intermediate Certificate(s): CA intermediate certificates
  • Root Certificate: CA root certificate (usually in browser)

Combine Certificate Chain

Most web servers need certificate + intermediate chain:

# Method 1: Concatenate files
cat /tmp/example.com.crt /tmp/intermediate.crt > /etc/ssl/certs/example.com-fullchain.crt

# Method 2: If CA provides bundle
cat /tmp/example.com.crt /tmp/ca-bundle.crt > /etc/ssl/certs/example.com-fullchain.crt

# Method 3: Manual creation
sudo nano /etc/ssl/certs/example.com-fullchain.crt

Add in this order:

-----BEGIN CERTIFICATE-----
[Your Domain Certificate]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Intermediate Certificate 1]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Intermediate Certificate 2 if exists]
-----END CERTIFICATE-----

Verify Certificate Chain

# Verify certificate
sudo openssl x509 -in /etc/ssl/certs/example.com.crt -text -noout

# Verify private key matches certificate
sudo openssl rsa -in /etc/ssl/private/example.com.key -check
sudo openssl x509 -noout -modulus -in /etc/ssl/certs/example.com.crt | openssl md5
sudo openssl rsa -noout -modulus -in /etc/ssl/private/example.com.key | openssl md5
# MD5 hashes should match

# Verify certificate chain
sudo openssl verify -CAfile /etc/ssl/certs/ca-bundle.crt /etc/ssl/certs/example.com.crt

Apache SSL/TLS Configuration

Ubuntu/Debian Apache Configuration

Enable SSL module:

sudo a2enmod ssl
sudo systemctl restart apache2

Create SSL virtual host:

sudo nano /etc/apache2/sites-available/example.com-ssl.conf

Add configuration:

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    ServerAdmin [email protected]

    DocumentRoot /var/www/example.com/html

    # SSL Engine
    SSLEngine on

    # Certificate files
    SSLCertificateFile /etc/ssl/certs/example.com.crt
    SSLCertificateKeyFile /etc/ssl/private/example.com.key

    # If using separate intermediate certificate
    SSLCertificateChainFile /etc/ssl/certs/intermediate.crt

    # Or if using full chain
    # SSLCertificateFile /etc/ssl/certs/example.com-fullchain.crt

    # Modern SSL configuration
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off

    # OCSP Stapling
    SSLUseStapling On
    SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

    # Security headers
    <IfModule mod_headers.c>
        Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        Header always set X-Frame-Options "SAMEORIGIN"
        Header always set X-Content-Type-Options "nosniff"
        Header always set X-XSS-Protection "1; mode=block"
    </IfModule>

    <Directory /var/www/example.com/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/example-ssl-error.log
    CustomLog ${APACHE_LOG_DIR}/example-ssl-access.log combined
</VirtualHost>

# HTTP to HTTPS redirect
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

Enable site and reload:

sudo a2ensite example.com-ssl.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

CentOS/Rocky/AlmaLinux Apache Configuration

Install mod_ssl:

sudo yum install mod_ssl -y  # or dnf

Create SSL virtual host:

sudo nano /etc/httpd/conf.d/example.com-ssl.conf

Add same configuration as Ubuntu (adjust paths if needed).

Test and reload:

sudo httpd -t
sudo systemctl reload httpd

Nginx SSL/TLS Configuration

Ubuntu/Debian Nginx Configuration

Create SSL server block:

sudo nano /etc/nginx/sites-available/example.com-ssl

Add configuration:

# HTTPS server block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html index.htm;

    # SSL certificates
    ssl_certificate /etc/ssl/certs/example.com-fullchain.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # SSL configuration
    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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # SSL session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Diffie-Hellman parameter for DHE ciphersuites
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

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

    access_log /var/log/nginx/example-ssl-access.log;
    error_log /var/log/nginx/example-ssl-error.log;
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    return 301 https://$server_name$request_uri;
}

Generate Diffie-Hellman parameters (one-time, takes several minutes):

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Enable and reload:

sudo ln -s /etc/nginx/sites-available/example.com-ssl /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

CentOS/Rocky/AlmaLinux Nginx Configuration

Create configuration:

sudo nano /etc/nginx/conf.d/example.com-ssl.conf

Add same configuration as Ubuntu, then:

sudo nginx -t
sudo systemctl reload nginx

Certificate Renewal

Manual Renewal Process

When certificate expires:

  1. Generate new CSR (can reuse private key):
sudo openssl req -new \
  -key /etc/ssl/private/example.com.key \
  -out /tmp/example.com-renewal.csr \
  -subj "/C=US/ST=California/L=San Francisco/O=Example Inc/CN=example.com"
  1. Submit CSR to CA
  2. Download new certificate
  3. Install new certificate:
sudo cp /tmp/example.com-new.crt /etc/ssl/certs/example.com.crt
  1. Reload web server:
sudo systemctl reload apache2  # or nginx

Check Certificate Expiry

# Check certificate expiration date
sudo openssl x509 -in /etc/ssl/certs/example.com.crt -noout -enddate

# Check certificate details
sudo openssl x509 -in /etc/ssl/certs/example.com.crt -text -noout

# Check days until expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

Expiry Monitoring Script

sudo nano /usr/local/bin/check-ssl-expiry.sh

Add:

#!/bin/bash
CERT="/etc/ssl/certs/example.com.crt"
EXPIRY=$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))

if [ $DAYS_LEFT -lt 30 ]; then
    echo "WARNING: SSL certificate expires in $DAYS_LEFT days!"
    # Send email alert
    echo "Certificate expires in $DAYS_LEFT days" | mail -s "SSL Certificate Warning" [email protected]
else
    echo "SSL certificate is valid for $DAYS_LEFT more days"
fi

Make executable and add to cron:

sudo chmod +x /usr/local/bin/check-ssl-expiry.sh
sudo crontab -e

Add:

0 0 * * * /usr/local/bin/check-ssl-expiry.sh

Testing SSL/TLS Configuration

Command Line Testing

# Test SSL connection
openssl s_client -connect example.com:443 -servername example.com

# Check certificate chain
openssl s_client -connect example.com:443 -showcerts

# Test specific protocol
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# Test cipher
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'

# Full diagnostic
curl -I https://example.com

Online Testing Tools

Local Browser Testing

Access https://example.com and check:

  • Valid certificate (green lock icon)
  • Certificate details show correct information
  • No mixed content warnings
  • All resources loaded via HTTPS

Troubleshooting

Certificate Not Trusted

Symptoms: Browser shows "Not Secure" or certificate warning

Solutions:

  1. Verify certificate chain:
openssl verify -CAfile /etc/ssl/certs/ca-bundle.crt /etc/ssl/certs/example.com.crt
  1. Ensure using fullchain:
# Check if certificate file includes chain
sudo openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/example.com.crt | openssl pkcs7 -print_certs -noout
  1. Check intermediate certificates installed

Private Key Mismatch

Symptoms: Web server won't start or SSL errors

Solution:

# Verify key matches certificate
sudo openssl x509 -noout -modulus -in /etc/ssl/certs/example.com.crt | openssl md5
sudo openssl rsa -noout -modulus -in /etc/ssl/private/example.com.key | openssl md5
# MD5 hashes must match

Permission Errors

Symptoms: Web server can't read certificate files

Solution:

# Fix permissions
sudo chmod 644 /etc/ssl/certs/example.com.crt
sudo chmod 600 /etc/ssl/private/example.com.key
sudo chown root:root /etc/ssl/private/example.com.key

# Verify
ls -la /etc/ssl/private/example.com.key
ls -la /etc/ssl/certs/example.com.crt

Mixed Content Warnings

Symptoms: HTTPS page loads HTTP resources

Solution:

  • Update all resource URLs to HTTPS or relative URLs
  • Check for hardcoded HTTP URLs in HTML, CSS, JavaScript
  • Use Content-Security-Policy header to detect issues

Certificate Expired

Symptoms: Browser shows expired certificate error

Solution:

# Check expiry
sudo openssl x509 -in /etc/ssl/certs/example.com.crt -noout -enddate

# Renew certificate (see Certificate Renewal section)

Security Best Practices

Strong Private Key

# Generate strong 4096-bit RSA key
sudo openssl genrsa -out /etc/ssl/private/example.com.key 4096

# Or use ECDSA (smaller, faster, equally secure)
sudo openssl ecparam -genkey -name secp384r1 -out /etc/ssl/private/example.com-ecc.key

Secure Private Key Storage

# Restrictive permissions
sudo chmod 600 /etc/ssl/private/example.com.key
sudo chown root:root /etc/ssl/private/example.com.key

# Never commit to version control
# Add to .gitignore:
echo "*.key" >> .gitignore

# Backup encrypted
tar -czf - /etc/ssl/private/ | openssl enc -aes-256-cbc -out ssl-backup.tar.gz.enc

Regular Updates

  • Monitor certificate expiry dates
  • Renew certificates before expiration
  • Update SSL/TLS configuration for latest security standards
  • Review and update cipher suites regularly

Certificate Transparency

Check certificate in CT logs:

# Query CT logs
curl -s "https://crt.sh/?q=example.com&output=json" | jq

Best Practices Summary

Certificate Management

  • Generate strong private keys (2048-bit minimum, 4096-bit recommended)
  • Secure private keys with restrictive permissions (600)
  • Keep private keys encrypted when backing up
  • Never share private keys
  • Use separate keys for different domains/services

Configuration

  • Disable SSLv3, TLS 1.0, and TLS 1.1
  • Use modern cipher suites
  • Enable OCSP stapling
  • Implement HSTS headers
  • Enable HTTP/2

Monitoring

  • Set up expiry alerts (30 days before)
  • Regular SSL testing with online tools
  • Monitor certificate transparency logs
  • Keep documentation updated
  • Test renewals before expiry

Conclusion

Manual SSL/TLS configuration provides complete control over your certificate infrastructure. This guide has covered self-signed certificates for development, commercial certificate installation, proper chain configuration, web server setup for Apache and Nginx, certificate renewal processes, and comprehensive troubleshooting.

Key takeaways:

  • Self-signed certificates are perfect for development and testing
  • Commercial certificates require proper CSR generation and chain installation
  • Strong SSL/TLS configuration is essential for security
  • Regular monitoring prevents certificate expiry issues
  • Proper private key management is critical for security

Implement robust SSL/TLS configurations to protect user data, build trust, and meet compliance requirements. Continue exploring advanced topics like certificate pinning, CAA DNS records, multi-domain certificate management, and automated renewal systems for enterprise environments. Always follow security best practices and keep your SSL/TLS configurations updated for optimal protection.