SSL Certificate Management
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
| Format | Extension | Type | Contents | Use Case |
|---|---|---|---|---|
| PEM | .pem, .crt, .cer, .key | Text (Base64) | Certificate, key, or chain | Linux, NGINX, Apache |
| DER | .der, .cer | Binary | Certificate only | Java, Windows |
| PKCS#7 | .p7b, .p7c | Text or binary | Certificate + chain (no key) | Windows, certificate chains |
| PKCS#12 | .pfx, .p12 | Binary | Certificate + key + chain | Windows IIS, importable bundle |
| JKS | .jks | Binary | Java keystore | Java 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:
Server Certificate (
*.example.com.cer)- Subject:
CN=*.example.com - Contains public key
- Signed by intermediate CA
- Subject:
Private Key (
example.com.key)- RSA 2048-bit or 4096-bit
- Must match certificate’s public key
- Keep secure, never share
CA Chain (
chain.cerorca-bundle.crt)- Intermediate CA certificate(s)
- Links server cert to trusted root
- Required for trust verification
Full Chain (
fullchain.cer)- Server cert + intermediate cert(s)
- Used by some web servers
PKCS#7 Bundle (
.p7b)- Contains server cert + full CA chain
- No private key
- Used for Windows certificate stores
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:
- Open IIS Manager
- Select server node
- Double-click “Server Certificates”
- Click “Import…” in Actions pane
- Browse to PFX file, enter password
- 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).
Related Documentation
- NetBox Installation Guide - SSL configuration for NetBox
- Grafana Stack Setup - SSL for monitoring tools