Python script for renew certificate from ZeroSSL.
- Acme4ZeroSSL
I manage sh*tload of servers, including my profile page, apartment's HomeKit gateway, several Hentai@Home client. Also, some headless system based on Apache Tomcat, which don't support authentication via HTTP/HTTPS challenge file.
Even though I can update CNAME record through Cloudflare API, certificate downloading and install has to be done manually. Current certificate validity is 90 days, but as that period gets shorter, those process becomes more annoying and frequent.
Developed to automate renewal certificate with ZeroSSL REST API, pair with Cloudflare hosting DNS records for CNAME challenge.
DNS hosting
Currently support Cloudflare only.
Domains
Single Common Name (CN).
Or single CN with single Subject Alternative Name (SAN) pairs.
Doesn't support wildcard certificate.
Using JSON format file storage configuration. Configuration file must include following parameters:
{
"Telegram_BOTs":{
"Token": "",
"ChatID": ""
},
"CloudflareAPI":{
"Token": "",
"Mail": ""
},
"CloudflareRecords":{
"ZoneID": "",
"CNAMERecordsID": ["", ""]
},
"ZeroSSLAPI":{
"AccessKey": "",
"Cache": ""
},
"Certificate":{
"Domains": ["www.example.com", "example.com"],
"Country": "",
"StateOrProvince": "",
"Locality": "",
"Organization": "",
"OrganizationalUnit": "",
"Config": "",
"CSR": "",
"PendingPK": "",
"PK": "",
"CA": "",
"CAB": ""
},
"FileChallenge":{
"HTMLFilePath": ""
}
}Configuration file must include following parameters:
Telegram BOTS token
Storage BOTsTokeninsideTelegram_BOTs.
Chat channel IDstorage atChatID.
Cloudflare API Key
StorageCloudflare API TokeninsideCloudflareAPI.
API authy emailstorage atNote:
Please remove theBearerstring and blank.
Cloudflare Zone ID
StorageCloudflare Zone IDinsideZoneID.
CNAME records IDstorage atCNAMERecordsIDlist.If you only need ACME certification for a single domain name, simply keep only one DNS records ID inside
CNAMERecordsIDlist.
"CNAMERecordsID": ["XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"]ZeroSSL REST API Key
StorageZeroSSL Access KeyinsideZeroSSLAPI.
Storage ZeroSSL certificate verify data as JSON file atCache.
"AccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"Cache": "/Documents/script/cache.domain.json"Certificate
Storage dertificate's domainsDomains.
"Domains": ["www.example.com", "example.com"],If you only need to renew single domain name: Simply keep only one domain in
Domainslist inside.
"Domains": ["www.example.com"],Certificate signing request (CSR) configuration
Countryfollowing ISO 3166-1 standard.
StateOrProvincefor geographical information.
Localityfor geographical information.
Organizationis recommended to NGO or Personal Business.
OrganizationalUnitis recommended to NGO or Personal Business.
"Country": "JP",
"StateOrProvince": "Tokyo Metropolis",
"Locality": "Shimokitazawa",
"Organization": "STARRY",
"OrganizationalUnit": "Kessoku Bando",
Configis CSR configuration file for generate CSR.
CSRis Certificate signing request saving path.
"Config": "/Documents/script/domain.csr.conf",
"CSR": "/Documents/script/domain.csr",Certificate catalog also include active Private key and Certificates path.
Fail-safe: private key won't update until renewal Certificate was download, will storage as pending key (PendingPK).
"PendingPK": "/Documents/script/cache.domain.key",
"PK": "/var/certificate/private.key",
"CA": "/var/certificate/certificate.crt",
"CAB": "/var/certificate/ca_bundle.crt"FileChallenge
Files path for HTTP/HTTPS file challenge.
Usually is your Apache/Nginx webpage folder.
"HTMLFilePath": "/var/www/html/"For using CNAME challenge function, you need to domain registered with Cloudflare, or choice Cloudflare as DNS hosting service.
For safety:
Please modify the token’s permissions.only allowing DNS record editis is recommended.
Also make sure copy the secret to secure place.
Login ZeroSSL, go to Developer page, you will find your ZeroSSL API Key, make sure to copy the secret to a secure place.
If you suspect that your API Key has been compromised:
Please clickReset Keyand check is any unusual, or suspicious certificate been issued.
Using Telegram Bot, contect BotFather create new Bot accounts.
At this point chat channel wasn't created, so you can't find the ChatID. Running Message2Me function will receive 400 Bad Request from Telegram API, following message will printout:
2025-05-14 19:19:00 | Telegram ChatID is empty, notifications will not be sent.
You need to start the chat channel with that bot, i.e. say Hello the world to him. Then running GetChatID
import acme4zerossl
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
Tg = acme4zerossl.Telegram(ConfigFilePath)
Tg.GetChatID()Now ChatID will printout:
2025-05-14 19:19:18 | You ChatID is: XXXXXXXXX
Function CertificateInstall support webpage server restart when certificate was downloaded (optional).
Command type
Adding command toServerCommandwith list object.
Default isNone, after download certificate will skip webpage server reload or restart.
# Function
Rt.CertificateInstall(CertificateContent, ServerCommand)
# Default is None
ServerCommand = None
# Apache
ServerCommand = ['systemctl', 'reload', 'apache2']
# Nginx
ServerCommand = ['service', 'nginx', 'restart']Recommend using systemd.
systemd service file
Create service file/etc/systemd/system/acme_cname.servicefor systemd.
WorkingDirectory
/Documents/scriptprevent absolute/relative path issue.
ExecStart/usr/bin/python3depend on Python environment.
Path/Documents/script/script_cname.pyis acme script located.
[Unit]
Description=ACME Script under CNAME mode
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
WorkingDirectory=/Documents/script
ExecStart=/usr/bin/python3 /Documents/script/script_cname.py
Timer file
Next is timer file/etc/systemd/system/acme_cname.timer.
Following example running everyday 5:00 AM and 10 minutes after boot up.
[Unit]
Description=Run ACME Script everyday
[Timer]
OnCalendar=*-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable service
Enable timer and clean cache.
# Enable and start the timer
systemctl enable acme_cname.timer
systemctl start acme_cname.timer
# Reload
systemctl daemon-reload# Import as module
import acme4zerossl
# Alternative
import acme4zerossl as acmeimport acme4zerossl as acme
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
Cf = acme.Cloudflare(ConfigFilePath)
Cf.VerifyCFToken()Default Output
Show result's value as string only.
Enable fully result by usingDisplayVerifyResult
Cf.VerifyCFToken(DisplayVerifyResult = True)import acme4zerossl as acme
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
Cf = acme.Cloudflare(ConfigFilePath)
Cf.GetCFRecords()Default Output
Output isdictionaryobject contain fully Cloudflare dns records data belong specify Zone ID.
AddingFileOutputfor output JSON file.
FileOutput = "/Documents/script/records.cloudflare.json"
Cf.GetCFRecords(FileOutput)Demonstration script
script_cname.pyincluding Telegram BOTs notify and check validity date of certificate.
import acme4zerossl as acme
from time import sleep
from sys import exit
# Configuration file path
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
ServerCommand = None
# Script
def main():
Rt = acme.Runtime(ConfigFilePath)
Cf = acme.Cloudflare(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Create certificates signing request
ResultCreateCSR = Rt.CreateCSR()
if isinstance(ResultCreateCSR, bool):
raise Exception()
elif isinstance(ResultCreateCSR, int):
pass
# Sending CSR
VerifyRequest = Zs.ZeroSSLCreateCA()
# Function error
if isinstance(VerifyRequest, bool):
raise Exception()
# ZeroSSL REST API HTTP error
elif isinstance(VerifyRequest, int):
raise Exception()
# Phrasing ZeroSSL verify
elif isinstance(VerifyRequest, dict):
VerifyData = Zs.ZeroSSLVerifyData(VerifyRequest, Mode="CNAME")
# Check verify data
if isinstance(VerifyData, bool):
raise Exception()
elif isinstance(VerifyData, dict):
pass
# Update CNAME via Cloudflare API
if ("additional_domains") in VerifyData:
UpdatePayloads = [
VerifyData.get('common_name'),
VerifyData.get('additional_domains')]
elif ("additional_domains") not in VerifyData:
UpdatePayloads = [
VerifyData.get('common_name')]
# Update Cloudflare CNAME records
for UpdatePayload in UpdatePayloads:
ResultUpdateCFCNAME = Cf.UpdateCFCNAME(UpdatePayload)
# Function error
if isinstance(ResultUpdateCFCNAME, bool):
raise Exception()
# Cloudflare API HTTP error
elif isinstance(ResultUpdateCFCNAME, int):
raise Exception()
# Check CNAME update result
elif isinstance(ResultUpdateCFCNAME, dict):
ResultUpdateResult = ResultUpdateCFCNAME.get("success")
if ResultUpdateResult == True:
sleep(5)
elif ResultUpdateResult == False:
raise Exception()
else:
raise Exception()
else:
raise Exception()
# Wait DNS records update and active
sleep(30)
# Verify CNAME challenge
CertificateID = VerifyData.get("id","")
VerifyResult = Zs.ZeroSSLVerification(CertificateID, ValidationMethod="CNAME_CSR_HASH")
# Function error
if isinstance(VerifyResult, bool):
raise Exception()
# ZeroSSL REST API HTTP error
elif isinstance(VerifyResult, int):
raise Exception()
# Possible errors respon
elif isinstance(VerifyResult, dict) and ("error") in VerifyResult:
VerifyErrorStatus = VerifyResult.get("error",{}).get("type","Unknown Error")
raise Exception (VerifyErrorStatus)
# Check verify status
elif isinstance(VerifyResult, dict) and ("status") in VerifyResult:
VerifyStatus = VerifyResult.get("status")
# Verify successful, wait issued
if VerifyStatus == ("draft"):
raise Exception()
elif VerifyStatus == ("pending_validation"):
sleep(30)
# Verify successful and been issued
elif VerifyStatus == ("issued"):
sleep(5)
# Undefined error
else:
raise Exception()
# Download certificates
CertificateContent = Zs.ZeroSSLDownloadCA(CertificateID)
if isinstance(CertificateContent, bool):
raise Exception()
elif isinstance(CertificateContent, str):
raise Exception(CertificateContent)
elif isinstance(CertificateContent, dict) and ("certificate.crt") in CertificateContent:
sleep(5)
# Install certificate to server folder
ResultCheck = Rt.CertificateInstall(CertificateContent, ServerCommand)
if isinstance(ResultCheck, bool):
raise Exception()
elif isinstance(ResultCheck, int):
pass
elif isinstance(ResultCheck, (list,str)):
pass
else:
raise Exception ()
# Runtime
try:
main()
exit(0)
except Exception:
exit(1)Demonstration script
script_httpsfile.pyincluding Telegram BOTs notify and check validity date of certificate.
# -*- coding: utf-8 -*-
import acme4zerossl as acme
from time import sleep
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
ServerCommand = None
# Script
def main():
Rt = acme.Runtime(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Create certificates signing request
ResultCreateCSR = Rt.CreateCSR()
if isinstance(ResultCreateCSR, bool):
raise Exception()
elif isinstance(ResultCreateCSR, int):
pass
# Sending CSR
VerifyRequest = Zs.ZeroSSLCreateCA()
# Function error
if isinstance(VerifyRequest, bool):
raise Exception()
# ZeroSSL REST API HTTP error
elif isinstance(VerifyRequest, int):
raise Exception()
# Phrasing ZeroSSL verify
elif isinstance(VerifyRequest, dict):
VerifyData = Zs.ZeroSSLVerifyData(VerifyRequest, Mode="FILE")
# Check verify data
if isinstance(VerifyData, bool):
raise Exception()
elif isinstance(VerifyData, dict):
pass
# Validation file path and content
if ("additional_domains") in VerifyData:
ValidationFiles = [
VerifyData.get('common_name'),
VerifyData.get('additional_domains')]
elif ("additional_domains") not in VerifyData:
ValidationFiles = [
VerifyData.get('common_name')]
# Create validation file
for ValidationFile in ValidationFiles:
Rt.CreateValidationFile(ValidationFile)
# Wait for web server cahce
sleep(15)
# Verify file challenge
CertificateID = VerifyData.get("id")
VerifyResult = Zs.ZeroSSLVerification(CertificateID, ValidationMethod="HTTPS_CSR_HASH")
# Function error
if isinstance(VerifyResult, bool):
raise Exception()
# ZeroSSL REST API HTTP error
elif isinstance(VerifyResult, int):
raise Exception()
# Possible errors respon
elif isinstance(VerifyResult, dict) and ("error") in VerifyResult:
VerifyErrorStatus = VerifyResult.get("error",{})
ErrorType = VerifyErrorStatus.get("type", "Unknown Error")
raise Exception (ErrorType)
# Check verify status
elif isinstance(VerifyResult, dict) and ("status") in VerifyResult:
VerifyStatus = VerifyResult.get("status")
# Verify successful, wait issued
if VerifyStatus == ("draft"):
raise Exception()
elif VerifyStatus == ("pending_validation"):
sleep(30)
# Verify successful and been issued
elif VerifyStatus == ("issued"):
sleep(5)
# Undefined error
else:
raise Exception()
# Delete validation file
for ValidationFile in ValidationFiles:
Rt.CleanValidationFile(ValidationFile)
# Download certificates
CertificateContent = Zs.ZeroSSLDownloadCA(CertificateID)
if isinstance(CertificateContent, bool):
raise Exception()
elif isinstance(CertificateContent, str):
raise Exception(CertificateContent)
elif isinstance(CertificateContent, dict) and ("certificate.crt") in CertificateContent:
sleep(5)
# Install certificate to server folder
ResultCheck = Rt.CertificateInstall(CertificateContent, ServerCommand)
ExpiresDate = VerifyResult.get("expires")
if isinstance(ResultCheck, bool):
raise Exception()
elif isinstance(ResultCheck, int):
pass
elif isinstance(ResultCheck, (list,str)):
pass
else:
raise Exception ()
# Runtime
try:
main()
exit(0)
except Exception:
exit(1)Demonstration script
script_download.py.
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
# Script
def DownloadScript():
Rt = acme.Runtime(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash), or press ENTER using cache file: ")
# Download certificate payload
CertificateContent = Zs.ZeroSSLDownloadCA(CertificateID or None)
# Check
if isinstance(CertificateContent, bool):
raise Exception()
elif isinstance(CertificateContent, str):
raise Exception()
# Download certificate and save to folder
elif isinstance(CertificateContent, dict) and ("certificate.crt") in CertificateContent:
pass
ResultCheck = Rt.CertificateInstall(CertificateContent)
if isinstance(ResultCheck, bool):
raise Exception()
elif isinstance(ResultCheck, int):
Rt.Message("Certificate been downloaded to folder. You may need to restart server manually.")
elif isinstance(ResultCheck, (list,str)):
Rt.Message(f"Certificate been downloaded and server has reload or restart.")
# Runtime
try:
DownloadScript()
exit(0)
except Exception:
exit(1)Only certificates with status draft or
pending_validationcan be cancelled.
After verification, the certificatescannot been cancelled.
Demonstration script
Demonstration script namedscript_cancel.py.
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
# Script
def CancelScript():
Rt = acme.Runtime(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash): ")
# Cancel certificate
CancelStatus = Zs.ZeroSSLCancelCA(CertificateID)
# Status check, Error
if isinstance(CancelStatus, bool):
raise Exception()
# ZeroSSL REST API HTTP error
elif isinstance(CancelStatus, int):
raise Exception()
# Standard response, check status code
elif isinstance(CancelStatus, dict):
CancelResult = CancelStatus.get("success",{})
if CancelResult == 1:
Rt.Message(f"Certificate ID: {CertificateID} has been cancelled.")
elif CancelResult == 0:
Rt.Message("ZeroSSL REST API request successful, however unable cancel certificate.")
else:
raise Exception()
else:
raise Exception()
# Runtime
try:
CancelScript()
exit(0)
except Exception:
exit(1)Note
ZeroSSL REST API require reason for certificate revoke (Optional). Only certificates with statusissuedcan be revoked. If a certificate has already been successfully revoked you will get a success response nevertheless.
Demonstration script
Demonstration script namedscript_revoke.py.
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
# Script
def RevokeScript():
Rt = acme.Runtime(ConfigFilePath)
Revoke = acme.ZeroSSL(ConfigFilePath)
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash): ")
# Revoke certificate
RevokeStatus = Revoke.ZeroSSLRevokeCA(CertificateID)
# Status check
if isinstance(RevokeStatus, bool):
raise Exception()
elif isinstance(RevokeStatus, int):
raise Exception()
elif isinstance(RevokeStatus, dict) and ("success") in RevokeStatus:
RevokeResult = RevokeStatus.get("success","")
if RevokeResult == 1:
Rt.Message(f"Certificate ID: {CertificateID} has been revoked.")
elif RevokeResult == 0:
Rt.Message("ZeroSSL REST API request successful, however unable revoke certificate.")
else:
raise Exception()
else:
raise Exception()
# Runtime
try:
RevokeScript()
exit(0)
except Exception:
exit(1)Testing passed on above Python version:
- 3.12.11
- 3.9.6
- 3.9.2
- 3.7.3
- logging
- pathlib
- json
- datetime
- textwrap
- requests
- subprocess
- time
- sys
General Public License -3.0
- ZeroSSL REST APIdocumentation the official documentation.
- ZeroSSL-CertRenew for HTTP/HTTPS challenge file.