Introduction
Cloudflare Custom SSL certificates (Custom Certificates feature) allow you to upload your own SSL certificate instead of using Cloudflare's universal SSL. This is useful for extended validation certificates, certificates covering multiple domains, or certificates from specific issuers. Custom certificate errors can occur during upload, validation, or when the certificate doesn't cover all hostnames. Business and Enterprise plans support custom certificates; Pro plans have limited support.
Symptoms
- Custom certificate upload fails with format error
- Certificate shows "Pending" status indefinitely
- Error: "Certificate does not cover hostname"
- Certificate shows "Untrusted" status
- Hostnames return Cloudflare universal cert instead of custom
- Certificate renewal not updating in Cloudflare
- Certificate chain incomplete or missing intermediates
Common Causes
- Certificate format incorrect (PEM required)
- Private key mismatched with certificate
- Certificate doesn't cover all zone hostnames
- Missing intermediate certificate in chain
- Certificate expired or not yet valid
- Custom hostname not in certificate SAN
- Upload file encoding or line ending issues
- CAA record blocking certificate issuer
Step-by-Step Fix
- 1.Verify certificate format and requirements:
``` # Cloudflare requires: # - PEM format (base64 encoded with headers) # - Certificate chain including intermediates # - Matching private key (no passphrase) # - RSA or ECDSA keys supported
# Maximum sizes: # - Certificate: 16KB # - Private key: 8KB ```
- 1.Check certificate and key match:
```bash # Verify certificate modulus matches key modulus openssl x509 -noout -modulus -in certificate.pem | openssl md5 openssl rsa -noout -modulus -in private.key | openssl md5
# MD5 hashes should match exactly # If different, key doesn't match certificate ```
- 1.Verify certificate covers required hostnames:
```bash # Check certificate subject and SAN openssl x509 -noout -text -in certificate.pem | grep -E "Subject:|DNS:"
# Output shows: # Subject: CN = yourdomain.com # DNS:yourdomain.com, DNS:www.yourdomain.com
# All proxied hostnames must be in certificate ```
- 1.Prepare proper PEM format:
```bash # Concatenate certificate and chain in one file # Order: Leaf certificate, intermediate(s), root (optional)
cat your_certificate.pem intermediate.pem > bundle.pem
# Verify PEM format openssl x509 -in bundle.pem -noout -text
# Should parse without errors ```
- 1.Remove private key passphrase:
```bash # Private key must have no passphrase for Cloudflare upload
# Remove passphrase from key openssl rsa -in encrypted.key -out decrypted.key
# Enter passphrase when prompted # decrypted.key has no passphrase ```
- 1.Check certificate validity period:
```bash # Check certificate dates openssl x509 -noout -dates -in certificate.pem
# Output: # notBefore=Mar 1 00:00:00 2026 GMT # notAfter=Mar 1 00:00:00 2027 GMT
# Certificate must be currently valid (not expired, not future) ```
- 1.Verify intermediate certificates:
```bash # Check certificate chain completeness openssl verify -CAfile intermediate.pem certificate.pem
# Should return: certificate.pem: OK
# If error, chain incomplete # Download intermediate from certificate issuer ```
- 1.Upload via Cloudflare dashboard:
Navigate to: SSL/TLS > Custom Certificates
``` # Click "Add Custom Certificate" # Paste or upload: # - Certificate bundle (PEM with chain) # - Private key (PEM, no passphrase)
# Wait for validation ```
- 1.Upload via API:
```bash curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/custom_certificates" \ -H "Authorization: Bearer API_TOKEN" \ -H "Content-Type: application/json" \ --data '{ "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" }'
# Use proper JSON escaping for newlines (\n) ```
- 1.Check CAA records:
```bash # CAA records can block certificate usage dig yourdomain.com CAA +short
# CAA specifies which issuers can issue certificates # If CAA restricts issuers, custom cert must be from allowed issuer
# Remove CAA or ensure issuer matches ```
- 1.Verify custom hostname coverage:
Navigate to: SSL/TLS > Custom Hostnames (for SaaS providers)
# For custom hostnames on customers' domains:
# - Each hostname needs validation
# - HTTP or CNAME validation method
# - Certificate coverage must include custom hostname- 1.Monitor certificate status:
```bash # Check certificate status after upload curl -X GET "https://api.cloudflare.com/client/v4/zones/ZONE_ID/custom_certificates" \ -H "Authorization: Bearer API_TOKEN"
# Status values: # - initializing: Being validated # - pending_validation: Waiting for hostname validation # - active: Successfully deployed # - expired: No longer valid # - deleted: Removed ```
- 1.Test certificate deployment:
```bash # After deployment, verify certificate in browser curl -I https://yourdomain.com/
# Check certificate details openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -issuer -subject
# Should show your custom certificate issuer and subject ```
- 1.Handle certificate renewal:
``` # When custom certificate expires: # 1. Obtain new certificate from issuer # 2. Upload new certificate to Cloudflare # 3. Cloudflare automatically replaces old certificate
# Or use Cloudflare's automatic renewal for Custom Hostnames ```
Verification
After applying fixes:
- 1.Certificate status shows "Active" in Cloudflare dashboard
- 2.
openssl s_clientshows your custom certificate details - 3.All zone hostnames covered by certificate
- 4.Certificate chain complete (intermediates included)
- 5.No certificate warnings in browser
- 6.Certificate issuer matches expected provider