on 03-17-2025 05:36 AM
Have you ever wanted to get a public certificate for your ISE Portal, without the hassle of involving a commercial CA? If so, read on.
This lengthy guide (maybe better as a Youtube video ... in future) will show you how I setup a certbot on a Linux host to manage the lifecycle of an ISE Guest Portal system certificate. The goal was to move away from hassling with manual and costly annual public certificate renewals. In addition, if (or when) the industry moves to even shorter certificate lifespans (90 or 45 days), you won't care, because the robot is doing the work for you. Bring on the robots!!!
Disclaimer: it worked in my lab and there is a chance that if you follow this you might have different outcomes (using a different CA, or Linux or certbot version). The main point of this guide is to show what's involved, and I am confident that you can also roll your own solution out of this recipe.
Before we get started - this process contains concepts that you can explore in more details (e.g. Letsencrypt, certbot, ACME protocol etc) - I won't explain it here. And there is also an assumption that you can read/hack a Linux script to make it work for your purposes. The other pre-requisite is that you own or can manage a public DNS domain - either for your lab or your organisation. I registered a domain on Cloudflare for roughly $6(USD) per year which was perfect for my experimentation.
The goal is to automate the certificate renewal for an ISE Guest Portal using Letsencrypt's certbot. Certbot is an application that, implements the ACME protocol to automate certificate renewals. Other CAs (commercial) also support this. But Letsencrypt is free and by now the most popular CA in the world.
Feature request: ISE does not have any features that allow native integration with Letsencrypt - if it did, I would not be writing this how-to guide. But perhaps Cisco will consider this in future.
The Linux command is 'certbot' and once everything is installed/configured, it runs on its own and will renew the ISE Portal cert(s) every 60 days (as of now - Letsencrypt plans to introduce much shorter-lived certificates from April 2025 - watch this space). Let's get going!
I used a popular Linux distribution - Ubuntu Server 24.01 LTS but anything will do. This host must have internet access since certbot talks ACME protocol to Letsencrypt servers on the internet! Certbot can co-exist on an existing Linux server or just create another light-weight install that is dedicated to this one task. 1 vCPU, 1GB RAM, 20GB disk is more than enough. If you are super smart, you might even containerize this.
We need admin access to a public DNS domain provider to prove ownership of the DNS domain. In my example, I chose Cloudflare, and to keep my lab costs down, I chose the cheapest domain I could find at the time - 128bits.win (less than $10 a year to own the domain). Access the Admin portal at https://dash.cloudflare.com/
In the DNS Admin GUI, I created an API Token to allow editing of the 128bits.win domain - since certbot will use the API to create TXT records. Access the API config section at https://dash.cloudflare.com/profile/api-tokens
This API token will be stored on the Linux host that runs the certbot.
Once Ubuntu is installed, install certbot. I wanted the certbot application to run under the user 'letsencrypt' - therefore we create a separate user account called 'letsencrypt'
NB: I was unable to get certbot to execute/run automatically as this user - when it runs automatically, it always runs as user root. Certbot uses its own type of 'cronjob' to execute. Ideally would like to have the certbot automatically execute with a non-root user. I was unable to find a solution to this. In Ubuntu, I installed certbot as a 'snap' to get the latest version of certbot. In this case, certbot is executed as part of the systemd timer. You can view this if you are curious:
systemctl edit snap.certbot.renew.timer
Anyway, I created the user called 'letsencrypt' to store things like the ISE script, and also to store the curl secret credentials.
I created this user as part of the Ubuntu Server Installation process - but it can also be done subsequently if needed
sudo apt update
sudo apt upgrade
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
I am using DNS Plugin for Cloudflare integrations
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare
Login as the non-root user, and then create the directories for certbot, and the secrets:
mkdir ~/certbotconf
mkdir ~/certbotwork
mkdir ~/certbotlogs
mkdir ~/.secrets
nano ~/.secrets/.cloudflare.ini
In the .cloudflare.ini file, add the API key for cloudflare - e.g.:
dns_cloudflare_api_token = xyz12345
and save the file.
Create the CURL .netrc file that contain machine auth credentials with your favourite editor (like nano):
nano ~/.secrets/.netrc
Add the ISE FQDN and login creds to the .netrc - e.g.:
machine rnolabise01.rnlab.local login admin password $KeepMeSafe$
machine rnolabise02.rnlab.local login admin password $KeepMeSafe$
Make the files readable/writeable by the user only (of course root can also read these but …)
chmod 600 ~/.secrets/.netrc
chmod 600 ~/.secrets/.cloudflare.ini
Create the initial certbot skeleton files by running the following command:
certbot certonly --config-dir ~/certbotconf --work-dir ~/certbotwork --logs-dir ~/certbotlogs -v
Select option 1 (obtain cert via dns-cloudlfare)
Enter a valid email address
Pressy Y to agree to Terms of Service
Press N to not receive emails
Enter the domain for the cert - e.g. 128bits.win
Enter path to cloudflare .ini - /home/letsencrypt/.secrets/.cloudflare.ini
That should create the first cert! Congratulations.
Edit the certificate renewal file to change a default.
nano ~/certbotconf/renewal/128bits.win.conf
Add the lines shown below to the end of the file
#Added manually to make the solution a bit more tolerant
dns_cloudflare_propagation_seconds = 30
We now have a solution that updates the cert every 60 days and provides you with a private key and the certificate on that host. The next step is to get that private key and certificate installed in ISE. For this we need a regular shell script.
Create the directory and the script:
mkdir ~/ise_portal_auto_cert
nano ~/ise_portal_auto_cert/import_ise_port_cert.sh
The contents of the import_ise_port_cert.sh script. You should read through every line and edit any of the variables to suit your environment:
#!/bin/sh
#
# This script can be called to perform the ISE System Cert Import
# The recommended certbot hook is the deploy hook, but for testing you can also run it for the post hook.
# This script should be placed in the appropriate certbot directory - e.g. certbotconf/renewal-hooks/deploy
#
#########################################################
# User defined variables below #
# NB: Script is executed as root user - do not use the #
# ~ (tilde) to reference relative path names ! #
# Rather refer to the absolute pathnames below #
#########################################################
# Portal Cert domain name
CertDomainName=128bits.win
# Base Directory of this script
thisscriptbase=/home/letsencrypt/ise_portal_auto_cert
# The ISE FQDN used for OpenAPI calles
ISEGatewayFQDN=rnolabise01.rnlab.local
# The ISE Hostname as defined in ISE
ISEHostname=rnolabise01
# Filename containing the Root CA used by ISE Admin System Certificate
ISECAFile=RNLAB-ROOTCA.pem
# Letsencrypt base directories. Currently no environment variables exist. There is a feature request in github
certbotconf=/etc/letsencrypt
certbotlogs=/var/log/letsencrypt
curlsecrets=/home/letsencrypt/.secrets
# This script should run after a new Letsencrypt cert has been successfully renewed
# Letsencrypt container currently does not support bash scripts - hence why we are using /bin/sh
# Append an entry to the log file
echo Script ran at `date` >> $thisscriptbase/import_ise_port_cert.log
#Strip the special characters from the certificate file
certdata=$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' $certbotconf/live/$CertDomainName/cert.pem)
# ISE expects the private key to be password protected - Letsencrypt priv keys are not password protected and don't pose a risk.
# Keeping this password secure is not an issue - it's just temporary and used during the REST call
privkeypass=SomeSecr3t
#Strip the special characters from the private key file after performing password protection on it - required by ISE API
certkey=$(openssl pkcs8 -topk8 -in $certbotconf/live/$CertDomainName/privkey.pem -passout pass:$privkeypass | awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}')
#Generate a unique Friendly Name since ISE expects unique Friendly Names. Importing with same Friendly Name will cause an error
friendlyname="Portal Cert from $(date)"
# Take note of the double quotes in the curl request. This is used instead of single quotes, since single quotes
# preserve the string as a literal - this would prevent variable substitution. As a result, all literal double quotes (as required
# by the JSON syntax) must be explicitly escaped. This is tricky and looks a bit messy, but it's 100% correct and unavoidable
curl -s --cacert $thisscriptbase/$ISECAFile --netrc-file $curlsecrets/.netrc -X 'POST' \
"https://$ISEGatewayFQDN:443/api/v1/certs/system-certificate/import" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d "{
\"admin\": false,
\"allowExtendedValidity\": true,
\"allowOutOfDateCert\": true,
\"allowPortalTagTransferForSameSubject\": true,
\"allowReplacementOfCertificates\": true,
\"allowReplacementOfPortalGroupTag\": true,
\"allowRoleTransferForSameSubject\": true,
\"allowSHA1Certificates\": true,
\"allowWildCardCertificates\": false,
\"data\": \"$certdata\",
\"eap\": false,
\"ims\": false,
\"name\": \"$friendlyname\",
\"password\": \"$privkeypass\",
\"portal\": true,
\"portalGroupTag\": \"My Default Portal Certificate Group\",
\"privateKeyData\": \"$certkey\",
\"pxgrid\": false,
\"radius\": false,
\"saml\": false,
\"validateCertificateExtensions\": false
}"
#####################
# Cleanup task. #
#####################
# Attempt to delete any Portal Certificate remaining in "Not in use" status.
# In ISE 3.4 the API call does not support filtering by "usedBy" id - I had to devise my own method.
# Fetch all the cert IDs that where the "Issued To" field contains your Portal domain and use the 'jq' tool to extract the IDs
# It will return a list, if there are multiple certs
currentcerts=`curl -s --cacert $thisscriptbase/$ISECAFile --netrc-file $curlsecrets/.netrc \
-X 'GET' "https://$ISEGatewayFQDN:443/api/v1/certs/system-certificate/$ISEHostname?filter=issuedTo.CONTAINS.$CertDomainName" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' | jq ".response.[] | select(.usedBy==\"Not in use\" and .issuedTo==\"$CertDomainName\")" | jq .id`
# Iterate over the list and delete the certs from ISE
# Strip the double quotes before executing the for loop
cleancurrentcerts=`echo $currentcerts | tr -d \"`
for id in ${cleancurrentcerts};
do
curl -s --cacert $thisscriptbase/$ISECAFile --netrc-file $curlsecrets/.netrc \
-X 'DELETE' "https://$ISEGatewayFQDN:443/api/v1/certs/system-certificate/$ISEHostname/$id" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d "{\"allowWildcardDelete\": false}"
done
chmod +x ~/ise_portal_auto_cert/import_ise_port_cert.sh
ln -s ~/ise_portal_auto_cert/import_ise_port_cert.sh ~/certbotconf/renewal-hooks/deploy/import_ise_port_cert.sh
Easy peasy ...
Create the file below and paste in the Chain in PEM format - it's a once off process to allow the curl command to trust the ISE cert.
nano ~/ise_portal_auto_cert/RNLAB-ROOTCA.pem
We need the Letsencrypt CA cert chain installed in ISE prior to the cert renewal process
Ensure that you have the Root and Issuing public CA certs installed (they differ based on whether you use RSA or ECC certs)
You can keep an eye on certificate operations in the ISE Audit Reports.
Operations > Reports > Audit >OpenAPI Operations
As root user, the first command will list your certificate status, and the second command will simulate a renewal:
certbot certificates
certbot renew --dry-run -v
During testing is was useful to force a certificate renewal process. This will make a new certificate before it is due. Beware though, Letsencrypt has limits on how often you can do that.
certbot renew --force-renewal -v
Hello,
Thankyou for sharing this information!
Best regards,
Hazel
e-zpassct
Great article @Arne Bier!
I was trying to follow your instructions but there is no EasyDNS plugin for certbot for snap install. Someone wrote a plugin for pip install but pip install doesn't work on the latest Ubuntu. Posh ACME in Powershell is the only option for me and they have a good plugin for EasyDNS. The only challenge is how to escape the double quotes when running curl.exe in Powershell. I tried backslash (\), backtick(`) and ("") etc, and the only thing worked for me is (\`).
Question for you: this script installs LE cert on my PAN only. I had to run the curl command against my second node to install the same cert. Does the API allow cert import to multiple nodes?
Regards,
Simon
Hi @Simon Z
I'm glad you found the article useful. I did all of my development in Linux - as you experienced, there will be additional challenges to adapt this to other envitonments - but the concepts remain the same.
As for importing the system certificate into other nodes, I should have mentioned that - thanks for raising that. According to the Swagger OpenAPI documentation, you point the URL to the ISE node in question.
Swagger API is available via ISE Menu: Administration > System > Settings > API Settings
I haven't tried this myself. I only have two nodes in the lab and I am sure it would work. But for fully distributed deployments, you'd want to import the certificate directly into a PSN - and that node probably doesn't have API Gateway enabled.
Keep an eye on the official Cisco TME (Charlie Moreton) ISE Livestream "ISE Certificates with Let's Encrypt" coming soon.
Tuesday, May 27, 2025 - 8:00 am USA Pacific Time - register at https://cs.co/ise-webinars
I was hoping Cisco would do such a video and improve on what I have discussed in this tech article - if there is a Q&A and if you can join live (I can't) then it would be great to raise that question.
Yes it does work for my second node even though API gateway is not enabled on it.
Thanks for sharing the webnar info.
Hi @Arne Bier, I got some issues with renewal. Of course I had to use "Submit-Renewal -Force" in Posh-ACME. The first issue I ran into was some certfificate path issue. I figured it out as the new cert was issued by a different intermediate CA, which is R11 (the original one was by R10). Ok. I manually imported R11 into ISE so I now have X1 (root), R10 and R11 in Trusted Certificates. Then I got into another issue where curl complains about "same public key". If I delete the original certs then the script runs OK. I tried to manually import the new cert I got the following errors:
I am running ISE 3.4 P1. The new cert does have the same CN and SANs for sure, but does it have the same public key? The private key remains the same. Any thoughts?
Appreciate your help.
Simon
Isn't the whole point of renewing your certificate, that we generate a new public/private key pair as part of the process? I don't know what the valid use case is of retaining the keys, but to simply update the Valid From/To and the fingerprint. Does the new certificate have the same serial number?
No, two certs have different serial numbers and thumbprints. Unfortunately Posh-ACME re-uses the private key during each renewal BY DEFAULT. Fortunately I found out how to change that behavior by running “Set-PAOrder -AlwaysNewKey” in PowerShell (one time only). Once that done the script runs in a Task Scheduler beautifully. What a journey!
On a side note, according to this, Let’s Encrypt is in the middle of transitioning from Root X1 to X2. So I imported all root and intermediate certs (X1, X2, R10, R11, E5 and E6) into Trusted Certificates to avoid the hassle in the future.
Find answers to your questions by entering keywords or phrases in the Search bar above. New here? Use these resources to familiarize yourself with the community: