β Your Requirements:
- π Ports 80/443 are firewalled to Cloudflare IPs only
- βοΈ You're behind Cloudflare, so Let's Encrypt (Certbot) cannot reach port 80 directly
- π You're using Apache, not Nginx
- You already have a self-signed or manually managed cert in use
Since HTTP-01 won't work (because Let's Encrypt can't reach port 80 through Cloudflare's proxy), you must use the DNS-01 challenge, which updates DNS records temporarily during verification.
sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare- Go to Cloudflare API Tokens
- Click "Create Token"
- Use the "Edit zone DNS" template
- Assign it only to the domain example.com
- Save the API Token
Create a file to hold the API token:
sudo nano /etc/letsencrypt/cloudflare.iniPaste:
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKENSet correct permissions:
sudo chmod 600 /etc/letsencrypt/cloudflare.iniExample command (non-interactive):
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
--preferred-challenges dns-01 \
--agree-tos \
--no-eff-email \
--email your@email.com \
-d dev.example.com \
-d dev-api.example.com| Option | Description |
|---|---|
--dns-cloudflare |
Use Cloudflare DNS for challenge |
--dns-cloudflare-credentials |
Path to your Cloudflare API token file |
--dns-cloudflare-propagation-seconds 30 |
Wait 30s for DNS to propagate (Cloudflare is fast) |
--preferred-challenges dns-01 |
Force DNS-based validation |
--agree-tos |
Agree to Let's Encrypt terms automatically |
--no-eff-email |
Don't subscribe to EFF mailing list |
--email your@email.com |
Required by Let's Encrypt |
-d domain |
Domains to issue cert for |
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for dev.example.com
Waiting 30 seconds for DNS changes to propagate
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dev.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/dev.example.com/privkey.pem
This certificate expires on 2025-10-06.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
root@dev:/var/log/apache2# tail /var/log/letsencrypt/letsencrypt.log
2025-07-08 08:42:49,788:DEBUG:certbot._internal.display.obj:Notifying user:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dev.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/dev.example.com/privkey.pem
This certificate expires on 2025-10-06.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
2025-07-08 08:42:49,790:DEBUG:certbot._internal.display.obj:Notifying user: If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
Now update your <VirtualHost *:443> to use Certbot's live certs:
SSLCertificateFile /etc/letsencrypt/live/dev.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/dev.example.com/privkey.pem
# (No need for SSLCertificateChainFile with fullchain.pem)Save and reload Apache:
sudo systemctl reload apache2Certbot renews automatically via cron. To ensure Apache reloads after renewal, add this hook:
sudo crontab -eAdd:
0 2 * * * certbot renew --quiet --post-hook "systemctl reload apache2"This runs daily at 2 AM.
List the directory:
sudo ls -l /etc/letsencrypt/live/dev.example.com/You should see:
cert.pem
chain.pem
fullchain.pem
privkey.pem
Use openssl to inspect the certificate:
sudo openssl x509 -in /etc/letsencrypt/live/dev.example.com/fullchain.pem -text -nooutLook for:
- Issuer β should say Let's Encrypt
- Not Before and Not After β issue and expiry dates (valid for 90 days)
- DNS: β make sure all domains (e.g. dev.example.com, dev-api.example.com, etc.) are listed under Subject Alternative Name (SAN)
This shows expiry info for all managed certs:
sudo certbot certificatesExample output:
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: dev.example.com
Serial Number: 55c926cd11842f24f0c6ed069581a25e262
Key Type: ECDSA
Domains: dev.example.com
Expiry Date: 2025-10-06 02:14:10+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/dev.example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/dev.example.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Manually run the renewal command:
sudo certbot renew --dry-runIf all is well, you should see:
Renewing an existing certificate
Performing the following challenges:
dns-01 challenge for dev.example.com
Waiting for verification...
Cleaning up challenges
Cert not yet due for renewal
You can test with:
curl -Iv https://dev.example.comYou should see something like:
* SSL certificate verify ok.
* subject: CN=dev.example.com
* start date: Jul 8 08:00:00 2025 GMT
* expire date: Oct 6 08:00:00 2025 GMT
* issuer: C=US; O=Let's Encrypt...
| Step | Description |
|---|---|
| β 1 | Install Certbot with Cloudflare DNS plugin |
| β 2 | Generate Cloudflare API token (Edit DNS only) |
| β 3 | Save token to /etc/letsencrypt/cloudflare.ini |
| β 4 | Use certbot certonly --dns-cloudflare ... |
| β 5 | Update Apache to use live certs |
| β 6 | Add auto-renew with --post-hook reload |
Hey thanks, I was having a hard time with cloudflare's and letsencrypt's documentation but these instructions worked like a breeze.