Automating SSL renewal with Let’s Encrypt and OCI DNS
By: Benjamin Kroll | July 29, 2025 | security and developer tools
Automating website management tasks like SSL certificate renewal is a great way to save time and mitigate human error, particularly if you manage multiple websites. Learn how to automate SSL certificate renewal using Let’s Encrypt and Oracle Cloud Infrastructure DNS API.
Managing SSL certificates is a critical, but tedious, aspect of site management. The best option for efficiency and continuity is automating the SSL renewal process. This is even more pertinent given the announcement that by 2029, the maximum lifetime for a TLS certificate will be 47 days.
Fortunately, leading certificate authorities (CAs) like Let’s Encrypt offer automation tools and APIs to connect to your DNS provider, making the process more straightforward. However, it can still require detailed setup and maintenance for more complex sites.
We manage certificate renewal for many of our clients at Mugo Web, some of whom have hundreds of domains. Automation is key in these instances. Here, we go through an example of Let’s Encrypt certificate validation implemented with Oracle Infrastructure DNS for one such client using our multi-site custom CMS.
Implementation of automated SSL
Overall, the implementation process is pretty straightforward.
We’ll set up a new domain for Let’s Encrypt certification from the Certbot command line. This job uses a pre-validation hook to call the OCI DNS API, add a TXT record to the DNS zone, and trigger the SSL certificate generation. The CLI script then uses a post-validation hook to remove TXT files and handle other cleanup.
Certbot stores this information in its renewal configuration for the domain. We’ll just need to set up a cron job or other configuration to automate certification renewal moving forward.
Setting up a new domain
Using the Certbot command line, we run this script to initially create a new site’s certificate.
certbot certonly \
--manual \
--preferred-challenges=dns \
-m email@example.com \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos \
--manual-auth-hook /letsencrypt/dnsauthenticate.sh \
--manual-cleanup-hook /letsencrypt/dnscleanup.sh \
-d '*.example.com,example.com'
--preferred-challenges=dns tells Certbot that we will use DNS-based validation.
The authentication script
The authentication script – which we run both from the CLI on initial certificate generation and from the renewal configuration we'll discuss later – performs the following steps:
- Prepares the JSON payload containing the TXT record information.
- Calls the API via cURL to create and publish the authentication TXT record in the appropriate OCI DNS zone.
- Sleeps to give the updates time to propagate.
#!/bin/bash
# -e (exit when a command fails)
# -u (exit when trying to use undefined variable)
# -o pipefail (return the exit code of piped commands that error)
set -euo pipefail
#########
# OCID of the tenancy calls are being made in to
tenancy_ocid="<YOUR_OCI_TENANCY_ID_HERE>"
# OCID of the user making the rest call
user_ocid="<YOUR_OCI_USER_ID_HERE>"
# Path to the private PEM format key for this user
private_key_path="<YOUR_OCI_API_KEY_PEM_PATH_HERE>"
# Fingerprint of the private key for this user
fingerprint="<YOUR_OCI_API_KEY_FINGERPRINT_HERE>"
# Host you want to make the call against
host="dns.<YOUR_OCI_HOSTING_REGION_HERE>.oraclecloud.com"
#########
# Set zone and challenge domain
# https://cloud.oracle.com/dns/zones
zone="$(sed -E 's/^(www\.|(\*\.)?(dev\.|staging\.|production\.)|\*\.)(.+)/\4/g' <<< $CERTBOT_DOMAIN)"
domain="_acme-challenge.$CERTBOT_DOMAIN"
# Certbot validation string
validation=$CERTBOT_VALIDATION
# The OCI API endpoint and request parameters
# zone = zone name or OCID
# https://docs.oracle.com/en-us/iaas/api/#/en/dns/20180115/Records/GetDomainRecords
rest_api="/20180115/zones/$zone/records/$domain"
body="{\"items\": [{\"domain\": \"$domain\", \"rdata\": \"$validation\", \"rtype\": \"TXT\", \"ttl\": 120 } ] }"
# Add extra headers required for a POST/PUT request
content_sha256="$(echo -n $body | openssl dgst -binary -sha256 | openssl enc -e -base64)";
content_sha256_header="x-content-sha256: $content_sha256"
content_length="$(echo -n $body | wc -c | xargs)";
content_length_header="content-length: $content_length"
headers="(request-target) date host"
# Add any extra fields required for a POST/PUT
headers=$headers" x-content-sha256 content-type content-length"
content_type_header="content-type: application/json";
date=`date -u "+%a, %d %h %Y %H:%M:%S GMT"`
date_header="date: $date"
host_header="host: $host"
request_target="(request-target): put $rest_api"
# The order in the signing_string must match the order in the headers, including the extra POST fields
signing_string="$request_target\n$date_header\n$host_header"
# Add any extra fields required for a POST/PUT
signing_string="$signing_string\n$content_sha256_header\n$content_type_header\n$content_length_header"
signature=`printf '%b' "$signing_string" | openssl dgst -sha256 -sign $private_key_path | openssl enc -e -base64 | tr -d '\n'`
# Finally, call the API
curl -X PUT --data-binary "$body" -sS https://$host$rest_api -H "date: $date" -H "x-content-sha256: $content_sha256" -H "content-type: application/json" -H "content-length: $content_length" -H "Authorization: Signature version=\"1\",keyId=\"$tenancy_ocid/$user_ocid/$fingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\""
# Sleep to make sure the change has time to propagate over to DNS
sleep 60
The clean-up script
The clean-up script, or post-validation hook, is called via --manual-cleanup-hook in the CLI or
manual_cleanup_hook in the renewal configuration, and performs the following steps:
- Prepares the request parameters for the delete request.
- Specifies the DNS record to be deleted using the _acme-challenge prefix.
- Calls the API via cURL to delete the authentication TXT record created for the given domain.
#!/bin/bash
# -e (exit when a command fails)
# -u (exit when trying to use undefined variable)
# -o pipefail (return the exit code of piped commands that error)
set -euo pipefail
#########
# OCID of the tenancy calls are being made in to
tenancy_ocid="<YOUR_OCI_TENANCY_ID_HERE>"
# OCID of the user making the rest call
user_ocid="<YOUR_OCI_USER_ID_HERE>"
# Path to the private PEM format key for this user
private_key_path="<YOUR_OCI_API_KEY_PEM_PATH_HERE>"
# Fingerprint of the private key for this user
fingerprint="<YOUR_OCI_API_KEY_FINGERPRINT_HERE>"
# Host you want to make the call against
host="dns.<YOUR_OCI_HOSTING_REGION_HERE>.oraclecloud.com"
# Set zone and challenge domain
# https://cloud.oracle.com/dns/zones
zone="$(sed -E 's/^(www\.|(\*\.)?(dev\.|staging\.|production\.)|\*\.)(.+)/\4/g' <<< $CERTBOT_DOMAIN)"
domain="_acme-challenge.$CERTBOT_DOMAIN"
#########
# The OCI API endpoint and request parameters
# zone = zone name or OCID
# https://docs.oracle.com/en-us/iaas/api/#/en/dns/20180115/Records/GetDomainRecords
rest_api="/20180115/zones/$zone/records/$domain"
date=`date -u "+%a, %d %h %Y %H:%M:%S GMT"`
date_header="date: $date"
host_header="host: $host"
request_target="(request-target): delete $rest_api"
# The order in the signing_string must match the order in the headers
signing_string="$request_target\n$date_header\n$host_header"
headers="(request-target) date host"
signature=`printf '%b' "$signing_string" | openssl dgst -sha256 -sign $private_key_path | openssl enc -e -base64 | tr -d '\n'`
# Finally, call the API
curl -X DELETE -s https://$host$rest_api -H "date: $date" -H "Authorization: Signature version=\"1\",keyId=\"$tenancy_ocid/$user_ocid/$fingerprint\",algorithm=\"rsa-sha256\",headers=\"$headers\",signature=\"$signature\""
Renewal configuration and installation
Certbot stores the information we used to generate the SSL certificate in its renewal configuration for the certificate. This config includes the hooks and other options used during certificate creation.
To renew the certificates, we just use a simple cronjob:
0 0,12 * * * root certbot renew
Since the renewal process takes care of renewing the certificate itself, not installing it, two more steps are required:
- Installing the certificate initially by updating the relevant web server configuration, e.g. nginx or Apache.
- Configuring a post-deployment action to ensure the web server reloads to pick up the renewed certificate.
Step 2, the renewal install loop, can be executed in one of three models:
1. Specifying an option inline as part of the cronjob, as we did in our previous implementation. Essentially, you simply call the command to reload the webserver.
2. Setting a parameter in the certificate’s renewal configuration that calls the renewal hook.
# in /etc/letsencrypt/renewal/example.com.conf
[renewalparams]
renew_hook = systemctl reload nginx
3. Or, creating a script that Certbot executes as part of a successful renewal.
# in /etc/letsencrypt/renewal-hooks/deploy/renew.sh
systemctl reload nginx
Note that scripts in deploy/ run only after successful renewals.
A few SSL issues to keep in mind
That covers the basics of integrating Let’s Encrypt with OCI DNS for automated SSL renewal.
Of course, there are some other configurations you need to keep in mind as you set up these jobs, including:
- Be sure that your web server allows Certbot to write to the directories where certificates are stored.
- If any problems do arise in your automated renewal process, you can bughunt the Certbot logs, typically found in /var/log/letsencrypt/.
SSL renewal automation is a no-brainer
Automating basic website management tasks like SSL certificate renewal is a great way to save time and mitigate human error, particularly if you manage a large network of sites. In this post, I’ve walked through the process of automating Let’s Encrypt certificate renewal with Oracle's OCI DNS API, but you’ll find that the general principles outlined here apply to any CA and DNS provider. Setting up renewal automation should be a straightforward way to make your life as a website manager a little easier.