Tutorials

Using Certbot-DNS-CloudFlare to Generate Free SSL Certificates in Webmin

If you're operating a web server in 2024, two things are almost certain: one, you are managing it via a popular control panel, and two, you want to serve your content over HTTPS.

A third is likely also a given: you want to save money.

While Webmin may be a bit long-in-the-tooth these days, it's still a common control panel of choice for administrators willing to trade flashy interfaces for something a little more basic and free. And coupled with Virtualmin, it even feels relatively modern, with nearly all the creature comforts you'd need to manage a server without ever touching an SSH terminal or SFTP.

However, there are plenty of areas where both Webmin and Virtualmin are starting to show their age. One such area is SSL certificates. There is an interface to generate them via LetsEncrypt using certbot, but options are split between Webmin and Virtualmin modules and don't offer nearly the same set of features as Webmin's self-signed certificates (which are nearly useless for 99% of the modern web). The default LetsEncrypt support is fine for a perfectly standard setup where you're hosting flat files over a firewall with port 80 exposed. But in an industry now dominated by containers and ever-tightening security, there is a high chance you're interested in hosting something that doesn't fit that mold.

Typically, that's where certbot plugins come in. These provide alternate methods of verifying domain ownership that don't rely on the traditional method of storing unique key files on the server accessed over HTTP. The most common alternative is to use DNS-based authentication, in which you are requested to create a record on your domain containing the unique key instead. This effectively works around file access and firewall limitations, but with a key tradeoff: the certificate generation process becomes interactive, not automated. And in the case of Webmin, while certbot console output is displayed, there is no way to send back some input without logging into a console and doing the whole thing manually.

If only we could automate the part where the DNS record is created too...

Say hello to the certbot-dns-cloudflare plugin. CloudFlare is already one of the most popular DNS providers and domain registrars out there, so there's a good chance your traffic is already being routed through it. In that case, we can leverage the CloudFlare API to do all the work for us!

But... how can we tell Webmin to use the plugin instead of the default challenge method?

First off, we must ensure that certbot-dns-cloudflare is installed in the first place. Installation will differ slightly depending on your server's operating system, but popular Linux distributions like Ubuntu should have it in their included repositories. That makes installation as simple as:

sudo apt install python3-certbot-dns-cloudflare

As certbot is a Python application, the plugin can also be installed via pip:

pip install certbot-dns-cloudflare

That's the easy part. The trickier part is replacing the default Webmin certbot command with one that uses the plugin we just installed instead.

Webmin Configuration

Fortunately, Webmin at least provides an interface for us to do this at all. To find it, log in to your Webmin/Virtualmin instance and navigate to Webmin > Webmin Configuration > Settings (gear icon). Then, switch "Configuration Category" to "Let's Encrypt configuration". Here you'll find an option unintuitively named "Full path to Let's Encrypt client command", with the options "Find Automatically" and an input box for you to specify your own.

LetsEncrypt Configuration

In clearer terms, this is the executable file Webmin will run when certbot requests are made through the interface. This is really meant to handle the possibility that certbot isn't installed in the same place on every server, but technically, any executable file path can be inserted here. However, there's no way to customize the arguments Webmin passes into it! For that, we'll have to get creative and write a shell script (or Bash script) to do the job for us.

For security purposes, the script will be comprised of two files: the script itself, and a separate file for storing CloudFlare API credentials.

The certbot-dns-cloudflare plugin supports either a username + global API key or a custom API token. Because the credentials will be stored in plaintext on your server, it's best to use an API token so that no one nefarious can gain full access to your CloudFlare account. To create one, log in to CloudFlare, then navigate to the API Tokens page. Click "Create Token", then find "Custom Token" and choose "Get Started". Give your new token a name, then set the first allowed permission to "Zone", "DNS", "Edit".

CloudFlare API Token Configuration

This is the only permission needed for certbot, so leave other settings at their defaults and proceed to create the token.

Once the token is created, it's time to store it on the server. Create a new file named "certbot-cloudflare-credentials.ini" and set the INI key dns_cloudflare_api_token to your newly-generated token. For example:

# Cloudflare API credentials for certbot-cloudflare.sh
dns_cloudflare_api_token = abcdefghijklmnopqrstuvwxyz0123456789

Next, we need a script to execute certbot using the CloudFlare plugin with our API key. Create another file on your server named "certbot-cloudflare.sh" and modify permissions to make sure it is executable.

sudo chmod +x certbot-cloudflare.sh

Now, a simple version of our script would look something like this:

#!/bin/bash

# Declare Cloudflare-specific arguments before passing to Certbot
cloudflare_path=$(realpath $(dirname $0))
cloudflare_args="--dns-cloudflare --dns-cloudflare-credentials $cloudflare_path/certbot-cloudflare-credentials.ini"

# Pass Cloudflare and Webmin arguments to Certbot
certbot $cloudflare_args $@

This executes certbot with the --dns-cloudflare argument to specify we want to use the certbot-dns-cloudflare plugin, plus --dns-cloudflare-credentials followed by the path to our "certbot-cloudflare-credentials.ini" file containing the API key needed for the plugin to run. Finally, we end with $@ to catch any further arguments passed into the script and send those to certbot as well.

However, if we were to run this script through Webmin now, we would receive an error message. After all, Webmin is set up to use HTTP-based challenges, and certbot can only accept arguments for one challenge method at a time. That said, we can't leave out Webmin arguments entirely, otherwise certbot won't know which domain to register an SSL certificate for! What gives?

The solution isn't exactly pretty, but it's not as bad as it looks, either. What we need to do is parse out the arguments contained in $@ and remove any that conflict with certbot-dns-cloudflare. This leaves us with a script looking something like this:

#!/bin/bash

# Declare Cloudflare-specific arguments before passing to Certbot
cloudflare_path=$(realpath $(dirname $0))
cloudflare_args="--dns-cloudflare --dns-cloudflare-credentials $cloudflare_path/certbot-cloudflare-credentials.ini"

# Remove conflicting Webmin arguments before passing to Certbot
webmin_args=()
skip_value=false

for arg in "$@"; do
    if [ "$skip_value" = true ]; then
        # Skip excluded argument values
        skip_value=false
        continue
    fi

    case $arg in
        # Exclude conflicting arguments
        -a|--webroot-path|--config|--rsa-key-size)
            skip_value=true
            ;;
        # Include other arguments (-d|--duplicate|--force-renewal|--non-interactive|--agree-tos|--cert-name)
        *)
            webmin_args+=("$arg")
            ;;
    esac
done

# Pass Cloudflare and Webmin arguments to Certbot
certbot $cloudflare_args "${webmin_args[@]}"

This loops over all arguments passed into $@, checks for arguments we want to remove, then skips the argument plus any related value that follows. All other arguments get passed into a custom webmin_args array which is appended to our certbot command when all is said and done.

With this script saved, we now have our complete "certbot-cloudflare.sh" and "certbot-cloudflare-credentials.ini" files. These can be placed anywhere on your server, so long as Webmin has access permissions to them. A safe and memorable place like /etc/letsencrypt should do nicely. (Note that you will need to repeat the chmod command each time you relocate the script file.)

Once you've decided on a permanent home, simply copy the path to your "certbot-cloudflare.sh" into the "Full path to Let's Encrypt client command" box in Webmin and hit "Save".

And... that's it!

Enjoy SSL certificates generated anywhere in Webmin or Virtualmin, regardless of firewalls or public file access!