Running Certbot In Your Private Network With Cloudflare Dns
September 2025
Table of contents
Overview
Certbot is a wonderful tool for automatic the process of SSL certificate requests and renewal. On my Proxmox host I am running an nginx reverse proxy to provide secure https access to the management console.
On a public server running nginx, it is trivial to install Certbot’s nginx plugin and generate a certificate, however that method works by checking public DNS records and matching the local system’s IP address to the DNS A or AAAA record. This works great for public services; except I do not have any public IPv4 addresses and run my management services in the private 10.99.0.0/24 subnet.
This guide will go over how certificates can still be automatically generated, even on systems which do not have a public IP.
Installing Certbot
Certbot can be installed through the Debian apt repository using the command
sudo apt install certbot
You will also need curl and jq
sudo apt install curl jq
Once certbot is installed we can move on to setup up our manual DNS challenge scripts which will create a DNS record on our Cloudflare account automatically using an API key.
Create Scripts
Certbot can use two scripts for manual DNS challenges, manual-auth-hook and manual-cleanup-hook. The auth-hook will reach out to Cloudflare’s API and create the challenge record, while the cleanup-hook deletes the record. You will need a Cloudflare API key that has access to DNS. This can be generated by logging in, selecting Profile -> API Tokens -> Create Token. From the list of API token templates, select the Edit zone DNS option. Be sure to write down the token, as Cloudflare will not display it again.
On your server, navigate to the directory you want your scripts to be stored in. I will be using /usr/local/sbin/, as this direcotry is already in my path. CD into the directory:
cd /usr/local/sbin/
Create the bash script file manual-auth-hook.sh and manual-cleanup-hook.sh:
sudo touch ./manual-auth-hook.sh
sudo touch ./manual-cleanup-hook.sh
Before writing the script, we get to get our zone ID. This value will tell the Cloudflare API which zone it should make DNS changes to later. Use this one-line command to make a request to your Cloudflare account and get the zone ID. Make sure to change example.com to the domain you are setup certbot for, and put in your API token:
curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=EXAMPLE.COM" -H "Authorization: Bearer API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].id'
Using the ID we can now create the two scripts. Open up manual-auth-hook.sh in an editor and paste in the following script
#!/bin/bash
# ENV variables or secrets
CLOUDFLARE_API_TOKEN="API_TOKEN"
ZONE_ID="ZONE_ID"
# Certbot variables passed to the script
DOMAIN="${CERTBOT_DOMAIN}"
VALIDATION="${CERTBOT_VALIDATION}"
# Create the DNS record
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"type":"TXT","name":"_acme-challenge.'"${DOMAIN}"'","content":"'"${VALIDATION}"'","ttl":120}' \
| jq
# Sleep to allow DNS propagation
sleep 15
Save changes and open manual-cleanup-hook.sh. Paste in the cleanup script:
#!/bin/bash
set -euo pipefail
# ENV variables or secrets
CLOUDFLARE_API_TOKEN="API_TOKEN"
ZONE_ID="ZONE_ID"
# Certbot variables passed to the script
DOMAIN="${CERTBOT_DOMAIN}"
RECORD_NAME="_acme-challenge.${DOMAIN}"
echo "Looking up record ID for $RECORD_NAME ..."
# Find record ID
RECORD_ID=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=TXT&name=${RECORD_NAME}" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" \
| jq -r '.result[0].id')
if [[ -n "$RECORD_ID" && "$RECORD_ID" != "null" ]]; then
echo "Deleting DNS record $RECORD_NAME ($RECORD_ID)"
curl -s -X DELETE \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" | jq .
else
echo "No TXT record found for $RECORD_NAME — nothing to delete."
fi
# Optional: small delay to avoid rate limits
sleep 5
Once the two files have been created, make sure they have execute permissions using chmod:
sudo chmod +x /usr/local/sbin/manual-auth-hook.sh
sudo chmod +x /usr/local/sbin/manual-cleanup-hook.sh
Generate Certs
Now that the scripts have been created, we can generate our certificates. Because we are going to run the certbot command with the two scripts as arguments, it will setup a systemd service timer to automatically renew the certificates once they expire. Run the following certbot command to enable certificate generation, making sure to enter your domain and subdomain:
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--manual-auth-hook manual-auth-hook.sh \
--manual-cleanup-hook manual-cleanup-hook.sh \
-d my.domain.com
Certbot is now configured to automatically generate and renew certificates on your machine now.
Nginx Conf
In your server’s config file for nginx you need to specify the ssl_certificates that were generated.
server {
listen 80;
server_name example.com;
# Redirect unsecure HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8080; # set to where your server is being hosted
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Restart Nginx:
sudo systemctl restart nginx
Your server should now be up and running using https and a certificate generated from within your local private network. Make sure your private DNS server is settup with the new domain, or you will get a warning when connecting to the server.