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"],
"ValidityDays": 90,
"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.
"CNAMERecordsID": ["XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"]If you only need ACME certification for a single domain name, simply keep one 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"Well-known URIs: acme-challenge URI will automatically create by ACME script.
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
# Apache2, using systemd
ServerCommand = ['systemctl','reload','apache2.service']
# Nginx, using systemd
ServerCommand = ['systemctl','reload','nginx']
# Nginx, using init
ServerCommand = ['/etc/init.d/nginx','reload']Recommend using systemd.
systemd service file
Create service file/etc/systemd/system/acme.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
# Wait till network available
After=network-online.target
Wants=network-online.target
[Service]
# Run once every call
Type=oneshot
# Root
User=root
# Script folder absolute path
WorkingDirectory=/var/acme
# Python environment (interpreter) and script located
ExecStart=/usr/bin/python3 /var/acme/script_cname.py
# Log output
StandardOutput=journal
StandardError=journal
Timer file
Next is timer file/etc/systemd/system/acme.timer.
Following example running everyday 5:00 AM and 10 minutes after boot up and network available.
[Unit]
Description=Run ACME script everyday
[Timer]
OnCalendar=*-*-* 05:00:00
# Avoid skip cause by poweroff
Persistent=true
# Service name
Unit=acme.service
# Adding randomized delay
RandomizedDelaySec=10m
# Avoid inaccuracy
AccuracySec=1m
[Install]
WantedBy=timers.target
Enable service
Enable systemd timer and clean cache.
# Enable and start the timer
systemctl enable acme.timer
systemctl start acme.timer
# Reload systemd
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.
# -*- coding: utf-8 -*-
import acme4zerossl as acme
import logging
from time import sleep
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
# Server reload or restart command
ServerCommand = None
# Error handling
FORMAT = "%(asctime)s |%(levelname)s |%(message)s"
logging.basicConfig(level=logging.INFO,filename="acme4zerossl.log",filemode="a",format=FORMAT)
# Script
def main(VerifyRetry,Interval):
# Load object
Rt = acme.Runtime(ConfigFilePath)
Cf = acme.Cloudflare(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Create certificates signing request
ResultCreateCSR = Rt.CreateCSR()
if not isinstance(ResultCreateCSR,list):
raise RuntimeError("Error occurred during Create CSR and Private key.")
# Sending CSR
VerifyRequest = Zs.ZeroSSLCreateCA()
if not isinstance(VerifyRequest,dict):
raise RuntimeError("Error occurred during request new certificate.")
# Phrasing ZeroSSL verify
VerifyData = Zs.ZeroSSLVerifyData(VerifyRequest)
if not isinstance(VerifyData,dict):
raise RuntimeError("Error occurred during phrasing ZeroSSL verify data.")
CertificateID = VerifyData.get("id",None)
if CertificateID is None:
raise RuntimeError("Certificate hash is empty.")
# CNAME Update data
UpdatePayloads = [VerifyData['common_name']]
AdditionalDomains = VerifyData.get('additional_domains')
if AdditionalDomains:
UpdatePayloads.append(AdditionalDomains)
# Update Cloudflare hsoting CNAME records
for UpdatePayload in UpdatePayloads:
ResultUpdateCFCNAME = Cf.UpdateCFCNAME(UpdatePayload)
# Check CNAME update result
if not isinstance(ResultUpdateCFCNAME,dict):
raise RuntimeError("Error occurred during connect Cloudflare update CNAME.")
else:
sleep(5)
# Wait DNS records update and active
sleep(60)
# Verify CNAME challenge
VerifyResult = Zs.ZeroSSLVerification(CertificateID,ValidationMethod="CNAME_CSR_HASH")
if not isinstance(VerifyResult,str):
raise RuntimeError("Error occurred during verification.")
# Check verify status
if VerifyResult == "draft":
raise RuntimeError("Not verified yet.")
# Verify passed (Under CNAME and file validation, pending_validation means verify successful)
elif VerifyResult in ("pending_validation","issued"):
sleep(30)
# Download certificates, adding retry and interval in case backlog certificate issuance
for _ in range(VerifyRetry):
CertificateContent = Zs.ZeroSSLDownloadCA(CertificateID)
# Successful download certificates
if isinstance(CertificateContent,dict):
break
sleep(Interval)
else:
raise RuntimeError(f"Unable download certificate.")
# Undefined error
else:
raise RuntimeError(f"Unable to check verification status, currently verification status: {VerifyResult}")
# Install certificate to server folder
ResultCheck = Rt.CertificateInstall(CertificateContent,ServerCommand)
if ResultCheck is False:
raise RuntimeError("Error occurred during certificate install. You may need to download and install manually.")
else:
return
# Runtime
if __name__ == "__main__":
try:
main(10,60)
logging.info("Certificate has been renewed")
exit(0)
# Ctrl+C manually stop
except KeyboardInterrupt:
logging.warning("Manually interrupt")
exit(0)
except Exception as RenewedError:
logging.exception(f"Script error| {RenewedError}")
exit(1)Demonstration script
script_httpsfile.pyincluding Telegram BOTs notify and check validity date of certificate.
# -*- coding: utf-8 -*-
import acme4zerossl as acme
import logging
from time import sleep
from sys import exit
# Config
ConfigFilePath = "/Documents/script/acme4zerossl.config.json"
# Server reload or restart command
ServerCommand = None
# Error handling
FORMAT = "%(asctime)s |%(levelname)s |%(message)s"
logging.basicConfig(level=logging.INFO,filename="acme4zerossl.log",filemode="a",format=FORMAT)
# Script
def main(VerifyRetry,Interval):
Rt = acme.Runtime(ConfigFilePath)
Zs = acme.ZeroSSL(ConfigFilePath)
# Create certificates signing request
ResultCreateCSR = Rt.CreateCSR()
if not isinstance(ResultCreateCSR,list):
raise RuntimeError("Error occurred during Create CSR and Private key.")
# Sending CSR
VerifyRequest = Zs.ZeroSSLCreateCA()
if not isinstance(VerifyRequest,dict):
raise RuntimeError("Error occurred during request new certificate.")
# Phrasing ZeroSSL verify
VerifyData = Zs.ZeroSSLVerifyData(VerifyRequest,ValidationMethod="HTTPS_CSR_HASH")
if not isinstance(VerifyData,dict):
raise RuntimeError("Error occurred during phrasing ZeroSSL verify data.")
CertificateID = VerifyData.get("id",None)
if CertificateID is None:
raise RuntimeError("Certificate hash is empty")
# Validation file path and content
ValidationFiles = [VerifyData['common_name']]
AdditionalDomains = VerifyData.get("additional_domains")
if AdditionalDomains:
ValidationFiles.append(AdditionalDomains)
# Create validation file
for ValidationFile in ValidationFiles:
CreateValidationFileStatus = Rt.CreateValidationFile(ValidationFile)
if CreateValidationFileStatus is not True:
raise RuntimeError("Error occurred during create validation file")
# Cahce
sleep(60)
# Verify file challenge
VerifyResult = Zs.ZeroSSLVerification(CertificateID,ValidationMethod="HTTPS_CSR_HASH")
if not isinstance(VerifyResult,str):
raise RuntimeError("Error occurred during file verification.")
# Check verify status
if VerifyResult == "draft":
raise RuntimeError("Not verified yet.")
# Verify passed (Under CNAME and file validation, pending_validation means verify successful)
elif VerifyResult in ("pending_validation","issued"):
sleep(30)
# Download certificates, adding retry and interval in case backlog certificate issuance
for _ in range(VerifyRetry):
CertificateContent = Zs.ZeroSSLDownloadCA(CertificateID)
# Successful download certificates
if isinstance(CertificateContent,dict):
break
sleep(Interval)
else:
raise RuntimeError(f"Unable download certificate.")
# Undefined error
else:
raise RuntimeError(f"Unable to check verification status, undefined status: {VerifyResult}")
# Delete validation file
for ValidationFile in ValidationFiles:
Rt.CleanValidationFile(ValidationFile)
# Install certificate to server folder
ResultCheck = Rt.CertificateInstall(CertificateContent,ServerCommand)
if ResultCheck is False:
raise RuntimeError("Error occurred during certificate install. You may need to download and install manually.")
else:
return
# Runtime
if __name__ == "__main__":
try:
main(10,60)
logging.info("Certificate has been renewed")
exit(0)
# Ctrl+C manually stop
except KeyboardInterrupt:
logging.warning("Manually interrupt")
exit(0)
except Exception as RenewedError:
logging.exception(f"Script error| {RenewedError}")
exit(1)# -*- coding: utf-8 -*-
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/documents/script/acme4zerossl.config.json"
# Script
def DownloadScript(CertificateID):
Rt = acme.Runtime(ConfigFilePath)
Download = acme.ZeroSSL(ConfigFilePath)
# Download certificate payload
CertificateContent = Download.ZeroSSLDownloadCA(CertificateID or None)
# Check
if not isinstance(CertificateContent, dict):
raise RuntimeError("Unable download certificate")
# Download certificate and save to folder
elif isinstance(CertificateContent, dict) and ("certificate.crt") in CertificateContent:
pass
ResultCheck = Rt.CertificateInstall(CertificateContent)
if isinstance is False:
raise RuntimeError("Error occurred during certificate install")
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:
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash), or press ENTER using cache file: ")
DownloadScript(CertificateID)
exit(0)
except Exception:
exit(1)Only certificates with status
draftorpending_validationcan be cancelled.
After verification, the certificatescannot been cancelled.
# -*- coding: utf-8 -*-
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/documents/script/acme4zerossl.config.json"
# Script
def CancelScript(CertificateID):
Rt = acme.Runtime(ConfigFilePath)
Cancel = acme.ZeroSSL(ConfigFilePath)
# Cancel certificate
CancelStatus = Cancel.ZeroSSLCancelCA(CertificateID)
# Status check, Error
if not isinstance(CancelStatus,dict):
# 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.")
else:
raise RuntimeError("Unable cancel certificate")
else:
raise RuntimeError("Error occurred during cancel certificate")
# Runtime
try:
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash): ")
CancelScript(CertificateID)
exit(0)
except Exception as ScriptError:
print(ScriptError)
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.
# -*- coding: utf-8 -*-
import acme4zerossl as acme
from sys import exit
# Config
ConfigFilePath = "/documents/script/acme4zerossl.config.json"
# Script
def RevokeScript(CertificateID):
Rt = acme.Runtime(ConfigFilePath)
Revoke = acme.ZeroSSL(ConfigFilePath)
# Revoke certificate
RevokeStatus = Revoke.ZeroSSLRevokeCA(CertificateID)
# Status check
if not isinstance(RevokeStatus,dict):
raise Exception()
elif isinstance(RevokeStatus,dict):
RevokeResult = RevokeStatus.get("success")
if RevokeResult == 1:
Rt.Message(f"Certificate ID: {CertificateID} has been revoked.")
else:
raise RuntimeError("Unable revoke certificate")
else:
raise RuntimeError("Error occurred during revoke certificate")
# Runtime
try:
# Input certificate hash manually
CertificateID = input("Please input certificate ID (hash): ")
RevokeScript(CertificateID)
exit(0)
except Exception as ScriptError:
print(ScriptError)
exit(1)Using self-signed certificate to prevent directly IP connecting leak domain certificate.
Demonstration script
stand alone, functionablescript_selfsigned.py.
Backup IP Address configuration at line 17 to 18.
CSR config: authority configuration at line 20 to 25.
Certificate filename and folder path: configuration at line 27, 29 and 30.
Server command: configuration at line 32, the same format as CertificateInstall.
# Backup IP address, if you really want
self.Address4Backup = ""
self.Address6Backup = None
# CSR config
self.Days = 60
self.Country = "JP"
self.State = "Tokyo Metropolis"
self.Locality = "Toshima"
self.Organization = "Tsukinomori Girl's Academy"
self.Unit = "Concert Band Club"
# Certificate folder path, None as default path
self.CertFolder = None
# Certificate and private key name
self.Certificate = "selfsigned_certificate.crt"
self.PrivateKey = "selfsigned_certificate.key"
# Server command
self.WebServer = NoneTesting passed on above Python version:
- 3.14.2
- 3.12.11
- 3.11.9
- 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.