01 Create Cloudflare API Token

Cloudflare dashboard > profile icon (top right) > My Profile > API Tokens > Create Token > use the "Edit zone DNS" template.

Settings: Permissions = Zone / DNS / Edit (pre-filled). Zone Resources = Include / Specific zone / yourdomain.com. One token per domain - scope it to only what you intend to automate.

Cloudflare shows this token exactly once. Copy it to Notepad before navigating away. If you lose it, you must create an entirely new token.

02 Store the Token on the Server
sudo mkdir -p /etc/letsencrypt/cloudflare sudo nano /etc/letsencrypt/cloudflare/credentials.ini

File contents (one line):

dns_cloudflare_api_token = YOUR_TOKEN_HERE

Save with Ctrl+X, Y, Enter. Then lock the file - it contains a live API token in plain text.

sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini

600 means only root can read this file.

03 Install the Certbot Cloudflare DNS Plugin
sudo dnf install python3-certbot-dns-cloudflare -y

This is a separate plugin from the certbot Virtualmin already uses. HTTP-01 proves domain ownership by answering a request on port 80. DNS-01 proves it by creating a TXT record via the Cloudflare API - which is why the orange cloud cannot block it.

04 Reissue Certificate via DNS-01 with Deploy Hook

Apache on this stack serves certs from Virtualmin's managed store, not from certbot's directory. The deploy hook below hands the cert to Virtualmin every time certbot renews. Certbot fetches, Virtualmin installs and reloads Apache. Set and forget.

The 60-second propagation flag is not optional. The default 10 seconds is too short for Cloudflare to publish the TXT record before Let's Encrypt checks for it. The request will fail without it.

sudo certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \ --dns-cloudflare-propagation-seconds 60 \ --deploy-hook "virtualmin install-cert --domain yourdomain.com \ --cert /etc/letsencrypt/live/yourdomain.com/cert.pem \ --key /etc/letsencrypt/live/yourdomain.com/privkey.pem \ --ca /etc/letsencrypt/live/yourdomain.com/chain.pem" \ -d yourdomain.com -d www.yourdomain.com

If a cert already exists from Video 1 you will get two prompts in order:

Prompt 1: Update key type or keep existing - choose K (Keep) to stay on RSA.

Prompt 2: Keep existing cert or renew and replace - choose 2 (Renew and replace) to switch to DNS-01.

Expect in output: TXT record created, verified, removed; "Successfully received certificate"; deploy hook fires and Virtualmin reports the cert installed.

05 Verify Apache is Serving the New Cert

First find your Virtualmin cert path - the directory name is a random ID per domain and will differ from the one in the video.

sudo find /etc/ssl/virtualmin -iname "ssl.cert"

Copy the full path from the output, then run:

sudo openssl x509 -noout -dates -in /etc/ssl/virtualmin/YOUR_ID/ssl.cert

Expect: notBefore = today's date. This is the file Apache actually reads. If it shows a date from before today, Apache is still serving the old cert while certbot renews one nobody reads - the exact silent failure this video fixes.

06 Turn Off Virtualmin Auto-Renew

Two renewal managers means one fails silently. Once the orange cloud is on, Virtualmin's port-80 method is the one that breaks. Certbot owns renewal now.

Virtualmin > your domain > SSL Certificate > SSL Providers > "Automatically renew certificate" - set to No, then click Save (not Request Certificate).

If yours already says No, that is fine - Virtualmin may have updated this automatically after the externally-issued cert landed.

07 Confirm Renewal Config Uses DNS-01
sudo cat /etc/letsencrypt/renewal/yourdomain.com.conf

Look for two things: authenticator = dns-cloudflare, and a renew_hook line containing the virtualmin install-cert command from section 04. Both should be present. If authenticator says webroot or anything else, edit the file and correct it.

08 Dry Run - Prove Renewal Will Work
sudo certbot renew --dry-run

Expect: "Congratulations, all simulated renewals succeeded." This proves renewal will work in 90 days instead of finding out when your site throws a security warning.

09 Origin Check - What Your Server Actually Serves

Once you are behind Cloudflare's proxy, the browser padlock only proves Cloudflare's cert is valid - not your server's. This command talks directly to your server IP, bypassing the proxy entirely, so you see exactly what Apache is serving.

openssl s_client -connect YOUR_SERVER_IP:443 -servername yourdomain.com /dev/null | openssl x509 -noout -dates -issuer

Expect: notBefore = today's date, issuer shows Let's Encrypt.

10 Enable the Orange Cloud

Check SSL/TLS mode BEFORE flipping the cloud. Cloudflare > SSL/TLS > Overview. If it says Flexible, switching the proxy on creates a redirect loop and takes your site down. It must say Full or Full (strict).

Cloudflare > DNS > Records > click Edit on the @ row > toggle Proxy status ON > Save. Repeat for the www row. It is Edit then toggle then Save - not clicking the cloud icon directly in the collapsed table view.

Once the orange cloud is on, https://yourdomain.com:10000 stops working. Cloudflare's proxy does not forward port 10000. Access Virtualmin directly by server IP: https://YOUR_SERVER_IP:10000. This is expected, not broken.

11 Bot Fight Mode

Cloudflare > your domain > Security > Settings > Bot Fight Mode > toggle On. Free on every plan. No configuration, no fine-tuning - that is a paid feature. It quietly cuts scripted probe traffic without you doing anything else.

12 Rate Limit Your Login Page

Cloudflare > Security > Security Rules > Rate Limiting Rules > Create Rule.

Name: "Rate limit login page". Field: URI Path. Operator: contains. Value: your login path (e.g. /wp-login.php for WordPress). Characteristics: IP (default). Rate: 5 requests. Period: 10 seconds (only option on free tier). Action: Block. Duration: 10 seconds (only option on free tier). Status: Active. Deploy.

Free tier gives you exactly one rate limiting rule slot. Spend it on your login page. After 5 hits in 10 seconds from one IP, that IP is blocked for 10 seconds - a hard block, not a throttle.

This rule covers your web app login page only. Cloudflare's proxy cannot see port 10000 traffic so it cannot protect the Virtualmin panel - access Virtualmin by IP directly.

13 Verify You Are Behind the Proxy
ping -n 4 yourdomain.com

Expect: the IP returned is a Cloudflare IP, not your server's IP. Your real server IP is now hidden.