SSL Certificate Management

6 October 2025 · Updated 6 October 2025

Working notes on SSL/TLS certificates: the common file formats, how to convert between them, and how to deploy them on NGINX, Apache, and IIS.

Certificate File Formats

Common Formats

FormatExtensionTypeContentsUse Case
PEM.pem, .crt, .cer, .keyText (Base64)Certificate, key, or chainLinux, NGINX, Apache
DER.der, .cerBinaryCertificate onlyJava, Windows
PKCS#7.p7b, .p7cText or binaryCertificate + chain (no key)Windows, certificate chains
PKCS#12.pfx, .p12BinaryCertificate + key + chainWindows IIS, importable bundle
JKS.jksBinaryJava keystoreJava applications

PEM Format Details

PEM files are Base64-encoded and contain:

Certificate:

-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAK...
-----END CERTIFICATE-----

Private Key:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w...
-----END PRIVATE KEY-----

RSA Private Key:

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----

Certificate Components

Wildcard Certificate Files

A typical wildcard certificate deployment includes:

  1. Server Certificate (*.example.com.cer)

    • Subject: CN=*.example.com
    • Contains public key
    • Signed by intermediate CA
  2. Private Key (example.com.key)

    • RSA 2048-bit or 4096-bit
    • Must match certificate’s public key
    • Keep secure, never share
  3. CA Chain (chain.cer or ca-bundle.crt)

    • Intermediate CA certificate(s)
    • Links server cert to trusted root
    • Required for trust verification
  4. Full Chain (fullchain.cer)

    • Server cert + intermediate cert(s)
    • Used by some web servers
  5. PKCS#7 Bundle (.p7b)

    • Contains server cert + full CA chain
    • No private key
    • Used for Windows certificate stores
  6. PKCS#12 Bundle (.pfx)

    • Server cert + private key + CA chain
    • Password protected
    • Portable, Windows-compatible

Certificate Format Conversion

Extract from PFX/PKCS#12

Extract certificate:

openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out certificate.crt

Extract encrypted private key:

openssl pkcs12 -in certificate.pfx -nocerts -out encrypted.key

Decrypt private key:

openssl rsa -in encrypted.key -out certificate.key
rm encrypted.key

Extract CA chain:

openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out chain.crt

One-liner to extract all:

# Certificate
openssl pkcs12 -in cert.pfx -clcerts -nokeys -out cert.crt

# Private key (unencrypted)
openssl pkcs12 -in cert.pfx -nocerts -nodes -out cert.key

# CA bundle
openssl pkcs12 -in cert.pfx -cacerts -nokeys -chain -out ca-bundle.crt

Create PFX from PEM

Combine certificate, key, and chain:

openssl pkcs12 -export -out certificate.pfx \
  -inkey certificate.key \
  -in certificate.crt \
  -certfile ca-bundle.crt \
  -passout pass:YourPassword

Create PFX without password (not recommended):

openssl pkcs12 -export -out certificate.pfx \
  -inkey certificate.key \
  -in certificate.crt \
  -certfile ca-bundle.crt \
  -passout pass:

Convert PEM to DER (Binary)

openssl x509 -in certificate.crt -outform DER -out certificate.der

Convert DER to PEM

openssl x509 -in certificate.der -inform DER -out certificate.crt

Extract Certificates from P7B

openssl pkcs7 -in certificate.p7b -print_certs -out certificates.pem

Certificate Verification

Verify Certificate and Key Match

Compare modulus (must match):

# Certificate modulus
openssl x509 -in certificate.crt -modulus -noout | openssl md5

# Private key modulus
openssl rsa -in certificate.key -modulus -noout | openssl md5

One-liner verification:

[ "$(openssl rsa -in certificate.key -modulus -noout | cut -d'=' -f2)" = \
  "$(openssl x509 -in certificate.crt -modulus -noout | cut -d'=' -f2)" ] && \
  echo "✓ Moduli match" || echo "✗ Moduli do NOT match"

Inspect Certificate Details

View certificate information:

openssl x509 -in certificate.crt -noout -text

Check expiration date:

openssl x509 -in certificate.crt -noout -enddate

View subject and issuer:

openssl x509 -in certificate.crt -noout -subject -issuer

Check SANs (Subject Alternative Names):

openssl x509 -in certificate.crt -noout -ext subjectAltName

Verify Certificate Chain

Check chain validity:

openssl verify -CAfile ca-bundle.crt certificate.crt

Verify against system trust store:

openssl verify certificate.crt

Test SSL/TLS Connection

Check remote server certificate:

openssl s_client -connect example.com:443 -servername example.com

Show certificate only:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -text

Check expiration:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
  openssl x509 -noout -enddate

NGINX Certificate Deployment

Basic HTTPS Configuration

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # Certificate and key
    ssl_certificate /etc/nginx/ssl/fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/certificate.key;

    # Modern 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';
    ssl_prefer_server_ciphers on;

    # SSL session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/ca-bundle.crt;

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

    location / {
        # Your application
    }
}

# HTTP redirect
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

Wildcard Certificate Configuration

server {
    listen 443 ssl http2;
    server_name *.example.com example.com;

    ssl_certificate /etc/nginx/ssl/wildcard.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/wildcard.example.com.key;

    # SSL configuration...
}

Certificate File Permissions

# Certificate (public) - readable by all
chmod 644 /etc/nginx/ssl/certificate.crt
chmod 644 /etc/nginx/ssl/fullchain.crt

# Private key - readable only by root/nginx
chmod 600 /etc/nginx/ssl/certificate.key
chown root:root /etc/nginx/ssl/certificate.key

Apache Certificate Deployment

Virtual Host Configuration

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/certificate.crt
    SSLCertificateKeyFile /etc/ssl/private/certificate.key
    SSLCertificateChainFile /etc/ssl/certs/ca-bundle.crt

    # Modern SSL configuration
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder on

    # OCSP stapling
    SSLUseStapling on
    SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

    DocumentRoot /var/www/html
</VirtualHost>

<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

Windows IIS Certificate Deployment

Import PFX Certificate

PowerShell:

# Import certificate to Local Machine store
$password = ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText
Import-PfxCertificate -FilePath "C:\Certs\certificate.pfx" -CertStoreLocation Cert:\LocalMachine\My -Password $password

GUI Method:

  1. Open IIS Manager
  2. Select server node
  3. Double-click “Server Certificates”
  4. Click “Import…” in Actions pane
  5. Browse to PFX file, enter password
  6. Click OK

Bind Certificate to Site

PowerShell:

# Get certificate thumbprint
$cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*example.com*"}
$thumbprint = $cert.Thumbprint

# Bind to website
New-WebBinding -Name "Default Web Site" -Protocol https -Port 443
$binding = Get-WebBinding -Name "Default Web Site" -Protocol https
$binding.AddSslCertificate($thumbprint, "My")

Bulk Update IIS Bindings

PowerShell script to update multiple sites:

param(
    [string]$SiteName = "*",
    [string]$CertThumbprint
)

# Get certificate
$cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Thumbprint -eq $CertThumbprint}

if (-not $cert) {
    Write-Error "Certificate with thumbprint $CertThumbprint not found"
    exit 1
}

# Get sites matching pattern
$sites = Get-Website | Where-Object {$_.Name -like $SiteName}

foreach ($site in $sites) {
    Write-Host "Updating SSL binding for site: $($site.Name)"

    # Get HTTPS bindings
    $bindings = Get-WebBinding -Name $site.Name | Where-Object {$_.protocol -eq "https"}

    foreach ($binding in $bindings) {
        # Update certificate
        $binding.AddSslCertificate($cert.Thumbprint, "My")
        Write-Host "  Updated binding: $($binding.bindingInformation)"
    }
}

Write-Host "Certificate update complete"

Let’s Encrypt Automation

Certbot (NGINX)

Install:

sudo apt install certbot python3-certbot-nginx

Obtain certificate:

sudo certbot --nginx -d example.com -d www.example.com

Auto-renewal:

# Test renewal
sudo certbot renew --dry-run

# Cron job (already created by certbot)
0 0,12 * * * root certbot renew --quiet

Certbot (Apache)

sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d example.com

ACME.sh (Alternative)

# Install
curl https://get.acme.sh | sh

# Obtain certificate (NGINX)
acme.sh --issue -d example.com -d www.example.com --nginx

# Install certificate
acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/key.pem \
  --fullchain-file /etc/nginx/ssl/cert.pem \
  --reloadcmd "systemctl reload nginx"

Certificate Monitoring

Expiration Monitoring Script

#!/bin/bash
# check-cert-expiry.sh

CERT_FILE="/etc/nginx/ssl/certificate.crt"
WARNING_DAYS=30

# Get expiration date
EXPIRY=$(openssl x509 -in "$CERT_FILE" -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))

echo "Certificate expires in $DAYS_LEFT days ($EXPIRY)"

if [ $DAYS_LEFT -lt $WARNING_DAYS ]; then
    echo "WARNING: Certificate expires soon!"
    # Send alert (email, Slack, etc.)
fi

Monitor Remote Certificate

#!/bin/bash
# check-remote-cert.sh

DOMAIN="example.com"
WARNING_DAYS=30

# Get certificate expiration
EXPIRY=$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 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 ))

echo "$DOMAIN certificate expires in $DAYS_LEFT days"

[ $DAYS_LEFT -lt $WARNING_DAYS ] && echo "WARNING: Expires soon!"

Things worth getting right

Use 2048-bit RSA as a minimum, 4096-bit for anything sensitive. Don’t go below.

Private keys should be readable only by root or the service account running the web server. chmod 600 on the key, service user as owner. Never commit private keys to version control.

PFX files get passwords. Use strong ones.

Monitor expiry and renew before it bites. Letting a cert expire in production is one of the fastest ways to take a service down.

Disable SSLv3, TLS 1.0, and TLS 1.1. TLS 1.2 and 1.3 only. Use modern cipher suites.

Turn on HSTS, OCSP stapling, and Certificate Transparency logging. These are all cheap wins.

Troubleshooting

Common Errors

“Certificate and key do not match”:

# Verify moduli match
openssl x509 -in cert.crt -modulus -noout | openssl md5
openssl rsa -in cert.key -modulus -noout | openssl md5

“Incomplete certificate chain”:

# Test with online tools or:
openssl s_client -connect example.com:443 -showcerts

“Certificate not trusted”: usually means the CA bundle isn’t installed, the chain order is wrong, or an intermediate certificate is missing.

“Certificate name mismatch”: check the SANs against the hostname being requested, and double-check wildcard syntax (*.example.com doesn’t cover foo.bar.example.com).