Weblogic ACME SSL Renewal
Automating SSL Renewals with Certbot on PeopleSoft Weblogic Servers
2025-10-18
Everything Oracle is a pain in the neck. Getting Certbot to work on Apache and Nginx took all of fifteen minutes, largely because EFF made the process extremely straightforward. But WebLogic took quite a bit more work to iron out. This post explains how we're doing it. Note that we assume a custom SSL certificate already exists and is being transitioned to an ACME-based renewal. If SSL has not been configured, refer to Oracle's documentation for initial setup. This is also referring specifically to PeopleSoft WebLogic servers. I assume many of the steps would be similar regardless of what application leverages Weblogic, but there are some environmental aspects that are specific to PeopleSoft here.
Step by Step
We'll start with some module imports and define some variables. In our case, we're injecting these as variables with Ansible, but they can be entered as static values as well.
#!/usr/bin/python3.9
import subprocess
import sys
import shutil
from datetime import datetime
import os
dom_alias = "ps_domain"
dom_fqdn = "ps_domain.lab.lcl"
store_pass = "supersecure"
cfg_dir = "/home/peoplesoft/certbot"
keystore = "/home/peoplesoft/webserv/peoplesoft/piaconfig/keystore/pskey"
err_log = f"{cfg_dir}/err_log.txt"
keytool = "/home/peoplesoft/java/bin"
psadmin = "home/peoplesoft/appserv/psadmin"
We'll also add a couple of helper functions to log error messages and wrap shell commands.
def log_error(message):
timestamp = datetime.now().isoformat()
with open(err_log, "a") as f:
f.write(f"{timestamp} - {message}\n")
def run_cmd(cmd, capture_output=False, check=False):
try:
if capture_output:
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
return result.stdout
else:
subprocess.run(cmd, check=check)
return ""
except subprocess.CalledProcessError as e:
output = (e.stdout or "") + (e.stderr or "")
log_error(f"Command failed: {' '.join(cmd)}\n{output}")
if check:
sys.exit(1)
return output
Next, we need to run certbot. In most cases, this will return the phrase not yet due for renewal in the output. In that case, we'll exit gracefully. If a new certificate is retrieved, we'll proceed to the next step of the script. If an error occurs, we'll log it and exit with failure code 1.
certbot_cmd = [
"certbot", "certonly",
"--webroot",
"--verbose",
"--agree-tos",
"--cert-name", dom_fqdn,
"--domain", dom_fqdn,
"--non-interactive",
"--config-dir", cfg_dir,
"--work-dir", cfg_dir,
"--logs-dir", cfg_dir
]
certbot_output = run_cmd(certbot_cmd, capture_output=True)
if "Certificate is saved at" in certbot_output:
pass
elif "Certificate not yet due for renewal" in certbot_output:
sys.exit(0)
else:
log_error("Certbot failed or returned unexpected output. Logging and exiting...")
log_error("Unexpected certbot output:\n" + certbot_output)
sys.exit(1)
To import the certificate into the keystore, we need a PKCS12 file. Before generating the PKCS12, we'll archive the previous file.
old_pfx = os.path.join(cfg_dir, "live", dom_fqdn, f"{dom_alias}.pfx")
old_pfx_backup = old_pfx + ".old"
if os.path.exists(old_pfx):
shutil.move(old_pfx, old_pfx_backup)
Next we'll run an openssl subprocess to generate the new PKCS12 file.
openssl_cmd = [
"openssl", "pkcs12", "-export",
"-in", os.path.join(cfg_dir, "live", dom_fqdn, "fullchain.pem"),
"-inkey", os.path.join(cfg_dir, "live", dom_fqdn, "privkey.pem"),
"-out", old_pfx,
"-passout", f"pass:{store_pass}",
"-name", f"{dom_alias}"
]
run_cmd(openssl_cmd, check=True)
Now, we're ready to interact with the keystore entries. First, we'll update the current alias with the -old suffix.
keytool_changealias_cmd = [
keytool, "-changealias",
"-keystore", keystore,
"-storepass", store_pass,
"-alias", dom_alias,
"-destalias", f"{dom_alias}-old"
]
run_cmd(keytool_changealias_cmd)
Then, we'll run another subprocess to import the new PKCS12 file containing our updated certificate.
keytool_import_cmd = [
keytool, "-importkeystore",
"-destkeystore", keystore,
"-deststorepass", store_pass,
"-srcstoretype", "PKCS12",
"-srckeystore", old_pfx,
"-srcstorepass", store_pass
]
run_cmd(keytool_import_cmd, check=True)
WebLogic will continue to present the previous SSL certificate until the service is restarted. To restart the service, we need to identify the running domain, issue the stop command, and then the subsequent start command. If your instance is managed with systemctl this step is a lot simpler.
psadmin_list_cmd = [psadmin, "-w", "list"]
domains_output = run_cmd(psadmin_list_cmd, capture_output=True)
domains = domains_output.strip().splitlines()
if not domains:
# No PeopleSoft domains found
pass
else:
for domain in domains:
status_cmd = [psadmin, "-w", "status", "-d", domain]
status_output = run_cmd(status_cmd, capture_output=True).strip()
if "started" in status_output:
# Stop the domain
shutdown_cmd = [psadmin, "-w", "shutdown", "-d", domain]
run_cmd(shutdown_cmd, check=True)
# Start the domain
start_cmd = [psadmin, "-w", "start", "-d", domain]
run_cmd(start_cmd, check=True)
else:
# Domain isn't running. Skip start command
pass
Next, we need to clean up by deleting both the old PKCS12 file and the alias in the Java keystore with the -old suffix.
# Delete old PKCS12 backup if exists
if os.path.exists(old_pfx_backup):
os.remove(old_pfx_backup)
# Remove previous alias from the keystore
keytool_delete_cmd = [
keytool, "-keystore", keystore,
"-storepass", store_pass,
"-delete",
"-alias", f"{dom_alias}-old"
]
run_cmd(keytool_delete_cmd)
You may want to add an additional step to verify new SSL certificate is presented on the HTTPS listener before cleaning up the previous entries. This can be done with another run_cmd that executes curl or openssh and parses the output for the new expiration date.
Full Script
Here's the full script.
#!/usr/bin/python3.9
import subprocess
import sys
import shutil
from datetime import datetime
import os
#region # VARIABLES #
dom_alias = "ps_domain"
dom_fqdn = "ps_domain.lab.lcl"
store_pass = "supersecure"
cfg_dir = "/home/peoplesoft/certbot"
keystore = "/home/peoplesoft/webserv/peoplesoft/piaconfig/keystore/pskey"
err_log = f"{cfg_dir}/err_log.txt"
keytool = "/home/peoplesoft/java/bin"
psadmin = "home/peoplesoft/appserv/psadmin"
#endregion # VARIABLES #
#region # FUNCTIONS #
def log_error(message):
timestamp = datetime.now().isoformat()
with open(err_log, "a") as f:
f.write(f"{timestamp} - {message}\n")
def run_cmd(cmd, capture_output=False, check=False):
try:
if capture_output:
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
return result.stdout
else:
subprocess.run(cmd, check=check)
return ""
except subprocess.CalledProcessError as e:
output = (e.stdout or "") + (e.stderr or "")
log_error(f"Command failed: {' '.join(cmd)}\n{output}")
if check:
sys.exit(1)
return output
#endregion # FUNCTIONS #
#region # EXECUTION #
def main():
# 1. Run certbot and capture output
certbot_cmd = [
"certbot", "certonly",
"--webroot",
"--verbose",
"--agree-tos",
"--cert-name", dom_fqdn,
"--domain", dom_fqdn,
"--non-interactive",
"--config-dir", cfg_dir,
"--work-dir", cfg_dir,
"--logs-dir", cfg_dir
]
certbot_output = run_cmd(certbot_cmd, capture_output=True)
if "Certificate is saved at" in certbot_output:
pass
elif "Certificate not yet due for renewal" in certbot_output:
sys.exit(0)
else:
log_error("Certbot failed or returned unexpected output. Logging and exiting...")
log_error("Unexpected certbot output:\n" + certbot_output)
sys.exit(1)
# 2. Move old PKCS12 file if it exists
old_pfx = os.path.join(cfg_dir, "live", dom_fqdn, f"{dom_alias}.pfx")
old_pfx_backup = old_pfx + ".old"
if os.path.exists(old_pfx):
shutil.move(old_pfx, old_pfx_backup)
# 3. Convert cert to PKCS12
openssl_cmd = [
"openssl", "pkcs12", "-export",
"-in", os.path.join(cfg_dir, "live", dom_fqdn, "fullchain.pem"),
"-inkey", os.path.join(cfg_dir, "live", dom_fqdn, "privkey.pem"),
"-out", old_pfx,
"-passout", f"pass:{store_pass}",
"-name", f"{dom_alias}"
]
run_cmd(openssl_cmd, check=True)
# 4. Rename the existing alias in the keystore
keytool_changealias_cmd = [
keytool, "-changealias",
"-keystore", keystore,
"-storepass", store_pass,
"-alias", dom_alias,
"-destalias", f"{dom_alias}-old"
]
run_cmd(keytool_changealias_cmd)
# 5. Import the new PKCS12 into the keystore
keytool_import_cmd = [
keytool, "-importkeystore",
"-destkeystore", keystore,
"-deststorepass", store_pass,
"-srcstoretype", "PKCS12",
"-srckeystore", old_pfx,
"-srcstorepass", store_pass
]
run_cmd(keytool_import_cmd, check=True)
# 6. Restart the web service domains
# Check for running domains
psadmin_list_cmd = [psadmin, "-w", "list"]
domains_output = run_cmd(psadmin_list_cmd, capture_output=True)
domains = domains_output.strip().splitlines()
if not domains:
# No PeopleSoft domains found
pass
else:
for domain in domains:
status_cmd = [psadmin, "-w", "status", "-d", domain]
status_output = run_cmd(status_cmd, capture_output=True).strip()
if "started" in status_output:
# Stop the domain
shutdown_cmd = [psadmin, "-w", "shutdown", "-d", domain]
run_cmd(shutdown_cmd, check=True)
# Start the domain
start_cmd = [psadmin, "-w", "start", "-d", domain]
run_cmd(start_cmd, check=True)
else:
# Domain isn't running. Skip start command
pass
# 7. Clean up - delete old PKCS12 backup if exists
if os.path.exists(old_pfx_backup):
os.remove(old_pfx_backup)
# 8. Remove previous alias from the keystore
keytool_delete_cmd = [
keytool, "-keystore", keystore,
"-storepass", store_pass,
"-delete",
"-alias", f"{dom_alias}-old"
]
run_cmd(keytool_delete_cmd)
if __name__ == '__main__':
main()
#endregion # EXECUTION #